diff --git a/contrib/draco/.cmake-format.py b/contrib/draco/.cmake-format.py index 64f2495b4..5b36f67aa 100644 --- a/contrib/draco/.cmake-format.py +++ b/contrib/draco/.cmake-format.py @@ -1,102 +1,137 @@ -# Generated with cmake-format 0.5.1 -# How wide to allow formatted cmake files -line_width = 80 - -# How many spaces to tab for indent -tab_size = 2 - -# If arglists are longer than this, break them always -max_subargs_per_line = 10 - -# If true, separate flow control names from their parentheses with a space -separate_ctrl_name_with_space = False - -# If true, separate function names from parentheses with a space -separate_fn_name_with_space = False - -# If a statement is wrapped to more than one line, than dangle the closing -# parenthesis on its own line -dangle_parens = False - -# What character to use for bulleted lists -bullet_char = '*' - -# What character to use as punctuation after numerals in an enumerated list -enum_char = '.' - -# What style line endings to use in the output. -line_ending = u'unix' - -# Format command names consistently as 'lower' or 'upper' case -command_case = u'lower' - -# Format keywords consistently as 'lower' or 'upper' case -keyword_case = u'unchanged' - -# Specify structure for custom cmake functions -additional_commands = { - "foo": { - "flags": [ - "BAR", - "BAZ" - ], - "kwargs": { - "HEADERS": "*", - "DEPENDS": "*", - "SOURCES": "*" - } +with section('parse'): + # Specify structure for custom cmake functions + additional_commands = { + 'draco_add_emscripten_executable': { + 'kwargs': { + 'NAME': '*', + 'SOURCES': '*', + 'OUTPUT_NAME': '*', + 'DEFINES': '*', + 'INCLUDES': '*', + 'COMPILE_FLAGS': '*', + 'LINK_FLAGS': '*', + 'OBJLIB_DEPS': '*', + 'LIB_DEPS': '*', + 'GLUE_PATH': '*', + 'PRE_LINK_JS_SOURCES': '*', + 'POST_LINK_JS_SOURCES': '*', + 'FEATURES': '*', + }, + 'pargs': 0, + }, + 'draco_add_executable': { + 'kwargs': { + 'NAME': '*', + 'SOURCES': '*', + 'OUTPUT_NAME': '*', + 'TEST': 0, + 'DEFINES': '*', + 'INCLUDES': '*', + 'COMPILE_FLAGS': '*', + 'LINK_FLAGS': '*', + 'OBJLIB_DEPS': '*', + 'LIB_DEPS': '*', + }, + 'pargs': 0, + }, + 'draco_add_library': { + 'kwargs': { + 'NAME': '*', + 'TYPE': '*', + 'SOURCES': '*', + 'TEST': 0, + 'OUTPUT_NAME': '*', + 'DEFINES': '*', + 'INCLUDES': '*', + 'COMPILE_FLAGS': '*', + 'LINK_FLAGS': '*', + 'OBJLIB_DEPS': '*', + 'LIB_DEPS': '*', + 'PUBLIC_INCLUDES': '*', + }, + 'pargs': 0, + }, + 'draco_generate_emscripten_glue': { + 'kwargs': { + 'INPUT_IDL': '*', + 'OUTPUT_PATH': '*', + }, + 'pargs': 0, + }, + 'draco_get_required_emscripten_flags': { + 'kwargs': { + 'FLAG_LIST_VAR_COMPILER': '*', + 'FLAG_LIST_VAR_LINKER': '*', + }, + 'pargs': 0, + }, + 'draco_option': { + 'kwargs': { + 'NAME': '*', + 'HELPSTRING': '*', + 'VALUE': '*', + }, + 'pargs': 0, + }, + # Rules for built in CMake commands and those from dependencies. + 'list': { + 'kwargs': { + 'APPEND': '*', + 'FILTER': '*', + 'FIND': '*', + 'GET': '*', + 'INSERT': '*', + 'JOIN': '*', + 'LENGTH': '*', + 'POP_BACK': '*', + 'POP_FRONT': '*', + 'PREPEND': '*', + 'REMOVE_DUPLICATES': '*', + 'REMOVE_ITEM': '*', + 'REVERSE': '*', + 'SORT': '*', + 'SUBLIST': '*', + 'TRANSFORM': '*', + }, + }, + 'protobuf_generate': { + 'kwargs': { + 'IMPORT_DIRS': '*', + 'LANGUAGE': '*', + 'OUT_VAR': '*', + 'PROTOC_OUT_DIR': '*', + 'PROTOS': '*', + }, + }, } -} -# A list of command names which should always be wrapped -always_wrap = [] +with section('format'): + # Formatting options. -# Specify the order of wrapping algorithms during successive reflow attempts -algorithm_order = [0, 1, 2, 3, 4] + # How wide to allow formatted cmake files + line_width = 80 -# If true, the argument lists which are known to be sortable will be sorted -# lexicographicall -autosort = False + # How many spaces to tab for indent + tab_size = 2 -# enable comment markup parsing and reflow -enable_markup = True + # If true, separate flow control names from their parentheses with a space + separate_ctrl_name_with_space = False -# If comment markup is enabled, don't reflow the first comment block in -# eachlistfile. Use this to preserve formatting of your -# copyright/licensestatements. -first_comment_is_literal = False + # If true, separate function names from parentheses with a space + separate_fn_name_with_space = False -# If comment markup is enabled, don't reflow any comment block which matchesthis -# (regex) pattern. Default is `None` (disabled). -literal_comment_pattern = None + # If a statement is wrapped to more than one line, than dangle the closing + # parenthesis on its own line. + dangle_parens = False -# Regular expression to match preformat fences in comments -# default=r'^\s*([`~]{3}[`~]*)(.*)$' -fence_pattern = u'^\\s*([`~]{3}[`~]*)(.*)$' + # Do not sort argument lists. + enable_sort = False -# Regular expression to match rulers in comments -# default=r'^\s*[^\w\s]{3}.*[^\w\s]{3}$' -ruler_pattern = u'^\\s*[^\\w\\s]{3}.*[^\\w\\s]{3}$' + # What style line endings to use in the output. + line_ending = 'unix' -# If true, emit the unicode byte-order mark (BOM) at the start of the file -emit_byteorder_mark = False + # Format command names consistently as 'lower' or 'upper' case + command_case = 'canonical' -# If a comment line starts with at least this many consecutive hash characters, -# then don't lstrip() them off. This allows for lazy hash rulers where the first -# hash char is not separated by space -hashruler_min_length = 10 - -# If true, then insert a space between the first hash char and remaining hash -# chars in a hash ruler, and normalize its length to fill the column -canonicalize_hashrulers = True - -# Specify the encoding of the input file. Defaults to utf-8. -input_encoding = u'utf-8' - -# Specify the encoding of the output file. Defaults to utf-8. Note that cmake -# only claims to support utf-8 so be careful when using anything else -output_encoding = u'utf-8' - -# A dictionary containing any per-command configuration overrides. Currently -# only `command_case` is supported. -per_command = {} + # Format keywords consistently as 'lower' or 'upper' case + keyword_case = 'upper' diff --git a/contrib/draco/.gitattributes b/contrib/draco/.gitattributes new file mode 100644 index 000000000..96acfc612 --- /dev/null +++ b/contrib/draco/.gitattributes @@ -0,0 +1 @@ +*.obj eol=lf \ No newline at end of file diff --git a/contrib/draco/.gitmodules b/contrib/draco/.gitmodules new file mode 100644 index 000000000..25f0a1c03 --- /dev/null +++ b/contrib/draco/.gitmodules @@ -0,0 +1,12 @@ +[submodule "third_party/googletest"] + path = third_party/googletest + url = https://github.com/google/googletest.git +[submodule "third_party/eigen"] + path = third_party/eigen + url = https://gitlab.com/libeigen/eigen.git +[submodule "third_party/tinygltf"] + path = third_party/tinygltf + url = https://github.com/syoyo/tinygltf.git +[submodule "third_party/filesystem"] + path = third_party/filesystem + url = https://github.com/gulrak/filesystem diff --git a/contrib/draco/BUILDING.md b/contrib/draco/BUILDING.md index d33917b88..340b2b83b 100644 --- a/contrib/draco/BUILDING.md +++ b/contrib/draco/BUILDING.md @@ -4,8 +4,10 @@ _**Contents**_ * [Mac OS X](#mac-os-x) * [Windows](#windows) * [CMake Build Configuration](#cmake-build-configuration) + * [Transcoder](#transcoder) * [Debugging and Optimization](#debugging-and-optimization) * [Googletest Integration](#googletest-integration) + * [Third Party Libraries](#third-party-libraries) * [Javascript Encoder/Decoder](#javascript-encoderdecoder) * [WebAssembly Decoder](#webassembly-decoder) * [WebAssembly Mesh Only Decoder](#webassembly-mesh-only-decoder) @@ -72,6 +74,43 @@ C:\Users\nobody> cmake ../ -G "Visual Studio 16 2019" -A x64 CMake Build Configuration ------------------------- +Transcoder +---------- + +Before attempting to build Draco with transcoding support you must run an +additional Git command to obtain the submodules: + +~~~~~ bash +# Run this command from within your Draco clone. +$ git submodule update --init +# See below if you prefer to use existing versions of Draco dependencies. +~~~~~ + +In order to build the `draco_transcoder` target, the transcoding support needs +to be explicitly enabled when you run `cmake`, for example: + +~~~~~ bash +$ cmake ../ -DDRACO_TRANSCODER_SUPPORTED=ON +~~~~~ + +The above option is currently not compatible with our Javascript or WebAssembly +builds but all other use cases are supported. Note that binaries and libraries +built with the transcoder support may result in increased binary sizes of the +produced libraries and executables compared to the default CMake settings. + +The following CMake variables can be used to configure Draco to use local +copies of third party dependencies instead of git submodules. + +- `DRACO_EIGEN_PATH`: this path must contain an Eigen directory that includes + the Eigen sources. +- `DRACO_FILESYSTEM_PATH`: this path must contain the ghc directory where the + filesystem includes are located. +- `DRACO_TINYGLTF_PATH`: this path must contain tiny_gltf.h and its + dependencies. + +When not specified the Draco build requires the presence of the submodules that +are stored within `draco/third_party`. + Debugging and Optimization -------------------------- @@ -114,17 +153,52 @@ $ cmake ../ -DDRACO_SANITIZE=address Googletest Integration ---------------------- -Draco includes testing support built using Googletest. To enable Googletest unit -test support the DRACO_TESTS cmake variable must be turned on at cmake -generation time: +Draco includes testing support built using Googletest. The Googletest repository +is included as a submodule of the Draco git repository. Run the following +command to clone the Googletest repository: + +~~~~~ bash +$ git submodule update --init +~~~~~ + +To enable Googletest unit test support the DRACO_TESTS cmake variable must be +turned on at cmake generation time: ~~~~~ bash $ cmake ../ -DDRACO_TESTS=ON ~~~~~ -When cmake is used as shown in the above example the googletest directory must -be a sibling of the Draco repository root directory. To run the tests execute -`draco_tests` from your build output directory. +To run the tests execute `draco_tests` from your build output directory: + +~~~~~ bash +$ ./draco_tests +~~~~~ + +Draco can be configured to use a local Googletest installation. The +`DRACO_GOOGLETEST_PATH` variable overrides the behavior described above and +configures Draco to use the Googletest at the specified path. + +Third Party Libraries +--------------------- + +When Draco is built with transcoding and/or testing support enabled the project +has dependencies on third party libraries: + +- [Eigen](https://eigen.tuxfamily.org/) + - Provides various math utilites. +- [Googletest](https://github.com/google/googletest) + - Provides testing support. +- [Gulrak/filesystem](https://github.com/gulrak/filesystem) + - Provides C++17 std::filesystem emulation for pre-C++17 environments. +- [TinyGLTF](https://github.com/syoyo/tinygltf) + - Provides GLTF I/O support. + +These dependencies are managed as Git submodules. To obtain the dependencies +run the following command in your Draco repository: + +~~~~~ bash +$ git submodule update --init +~~~~~ WebAssembly Decoder ------------------- diff --git a/contrib/draco/CMakeLists.txt b/contrib/draco/CMakeLists.txt index 6ea9b21fd..a93267d25 100644 --- a/contrib/draco/CMakeLists.txt +++ b/contrib/draco/CMakeLists.txt @@ -1,7 +1,18 @@ -cmake_minimum_required(VERSION 3.12 FATAL_ERROR) +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. -# Draco requires C++11. -set(CMAKE_CXX_STANDARD 11) +cmake_minimum_required(VERSION 3.12 FATAL_ERROR) project(draco C CXX) if(NOT CMAKE_BUILD_TYPE) @@ -10,21 +21,23 @@ endif() set(draco_root "${CMAKE_CURRENT_SOURCE_DIR}") set(draco_src_root "${draco_root}/src/draco") -set(draco_build "${Assimp_BINARY_DIR}") +set(draco_build "${CMAKE_BINARY_DIR}") if("${draco_root}" STREQUAL "${draco_build}") message( - FATAL_ERROR "Building from within the Draco source tree is not supported.\n" - "Hint: Run these commands\n" - "$ rm -rf CMakeCache.txt CMakeFiles\n" - "$ mkdir -p ../draco_build\n" "$ cd ../draco_build\n" - "And re-run CMake from the draco_build directory.") + FATAL_ERROR + "Building from within the Draco source tree is not supported.\n" + "Hint: Run these commands\n" + "$ rm -rf CMakeCache.txt CMakeFiles\n" + "$ mkdir -p ../draco_build\n" + "$ cd ../draco_build\n" + "And re-run CMake from the draco_build directory.") endif() -include(CMakePackageConfigHelpers) include(FindPythonInterp) include("${draco_root}/cmake/draco_build_definitions.cmake") include("${draco_root}/cmake/draco_cpu_detection.cmake") +include("${draco_root}/cmake/draco_dependencies.cmake") include("${draco_root}/cmake/draco_emscripten.cmake") include("${draco_root}/cmake/draco_flags.cmake") include("${draco_root}/cmake/draco_helpers.cmake") @@ -49,6 +62,7 @@ draco_track_configuration_variable(DRACO_GENERATED_SOURCES_DIRECTORY) # Controls use of std::mutex and absl::Mutex in ThreadPool. draco_track_configuration_variable(DRACO_THREADPOOL_USE_STD_MUTEX) + if(DRACO_VERBOSE) draco_dump_cmake_flag_variables() draco_dump_tracked_configuration_variables() @@ -68,29 +82,32 @@ draco_reset_target_lists() draco_setup_options() draco_set_build_definitions() draco_set_cxx_flags() +draco_set_exe_linker_flags() draco_generate_features_h() # Draco source file listing variables. -list(APPEND draco_attributes_sources - "${draco_src_root}/attributes/attribute_octahedron_transform.cc" - "${draco_src_root}/attributes/attribute_octahedron_transform.h" - "${draco_src_root}/attributes/attribute_quantization_transform.cc" - "${draco_src_root}/attributes/attribute_quantization_transform.h" - "${draco_src_root}/attributes/attribute_transform.cc" - "${draco_src_root}/attributes/attribute_transform.h" - "${draco_src_root}/attributes/attribute_transform_data.h" - "${draco_src_root}/attributes/attribute_transform_type.h" - "${draco_src_root}/attributes/geometry_attribute.cc" - "${draco_src_root}/attributes/geometry_attribute.h" - "${draco_src_root}/attributes/geometry_indices.h" - "${draco_src_root}/attributes/point_attribute.cc" - "${draco_src_root}/attributes/point_attribute.h") +list( + APPEND draco_attributes_sources + "${draco_src_root}/attributes/attribute_octahedron_transform.cc" + "${draco_src_root}/attributes/attribute_octahedron_transform.h" + "${draco_src_root}/attributes/attribute_quantization_transform.cc" + "${draco_src_root}/attributes/attribute_quantization_transform.h" + "${draco_src_root}/attributes/attribute_transform.cc" + "${draco_src_root}/attributes/attribute_transform.h" + "${draco_src_root}/attributes/attribute_transform_data.h" + "${draco_src_root}/attributes/attribute_transform_type.h" + "${draco_src_root}/attributes/geometry_attribute.cc" + "${draco_src_root}/attributes/geometry_attribute.h" + "${draco_src_root}/attributes/geometry_indices.h" + "${draco_src_root}/attributes/point_attribute.cc" + "${draco_src_root}/attributes/point_attribute.h") list( APPEND draco_compression_attributes_dec_sources "${draco_src_root}/compression/attributes/attributes_decoder.cc" "${draco_src_root}/compression/attributes/attributes_decoder.h" + "${draco_src_root}/compression/attributes/attributes_decoder_interface.h" "${draco_src_root}/compression/attributes/kd_tree_attributes_decoder.cc" "${draco_src_root}/compression/attributes/kd_tree_attributes_decoder.h" "${draco_src_root}/compression/attributes/kd_tree_attributes_shared.h" @@ -107,7 +124,7 @@ list( "${draco_src_root}/compression/attributes/sequential_normal_attribute_decoder.h" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_decoder.cc" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_decoder.h" - ) +) list( APPEND @@ -128,7 +145,7 @@ list( "${draco_src_root}/compression/attributes/sequential_normal_attribute_encoder.h" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_encoder.cc" "${draco_src_root}/compression/attributes/sequential_quantization_attribute_encoder.h" - ) +) list( @@ -160,7 +177,7 @@ list( "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_base.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_decoding_transform.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h" - ) +) list( APPEND @@ -192,7 +209,7 @@ list( "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_base.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_encoding_transform.h" "${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h" - ) +) list( APPEND @@ -217,27 +234,34 @@ list( "${draco_src_root}/compression/bit_coders/symbol_bit_encoder.cc" "${draco_src_root}/compression/bit_coders/symbol_bit_encoder.h") -list(APPEND draco_enc_config_sources - "${draco_src_root}/compression/config/compression_shared.h" - "${draco_src_root}/compression/config/draco_options.h" - "${draco_src_root}/compression/config/encoder_options.h" - "${draco_src_root}/compression/config/encoding_features.h") +list( + APPEND draco_enc_config_sources + "${draco_src_root}/compression/config/compression_shared.h" + "${draco_src_root}/compression/config/draco_options.h" + "${draco_src_root}/compression/config/encoder_options.h" + "${draco_src_root}/compression/config/encoding_features.h") -list(APPEND draco_dec_config_sources - "${draco_src_root}/compression/config/compression_shared.h" - "${draco_src_root}/compression/config/decoder_options.h" - "${draco_src_root}/compression/config/draco_options.h") +list( + APPEND draco_dec_config_sources + "${draco_src_root}/compression/config/compression_shared.h" + "${draco_src_root}/compression/config/decoder_options.h" + "${draco_src_root}/compression/config/draco_options.h") + +list(APPEND draco_compression_options_sources + "${draco_src_root}/compression/draco_compression_options.cc" + "${draco_src_root}/compression/draco_compression_options.h") list(APPEND draco_compression_decode_sources "${draco_src_root}/compression/decode.cc" "${draco_src_root}/compression/decode.h") -list(APPEND draco_compression_encode_sources - "${draco_src_root}/compression/encode.cc" - "${draco_src_root}/compression/encode.h" - "${draco_src_root}/compression/encode_base.h" - "${draco_src_root}/compression/expert_encode.cc" - "${draco_src_root}/compression/expert_encode.h") +list( + APPEND draco_compression_encode_sources + "${draco_src_root}/compression/encode.cc" + "${draco_src_root}/compression/encode.h" + "${draco_src_root}/compression/encode_base.h" + "${draco_src_root}/compression/expert_encode.cc" + "${draco_src_root}/compression/expert_encode.h") list( APPEND @@ -291,7 +315,7 @@ list( "${draco_src_root}/compression/point_cloud/point_cloud_kd_tree_decoder.h" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_decoder.cc" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_decoder.h" - ) +) list( APPEND @@ -302,112 +326,126 @@ list( "${draco_src_root}/compression/point_cloud/point_cloud_kd_tree_encoder.h" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_encoder.cc" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_encoder.h" - ) +) -list(APPEND draco_compression_entropy_sources - "${draco_src_root}/compression/entropy/ans.h" - "${draco_src_root}/compression/entropy/rans_symbol_coding.h" - "${draco_src_root}/compression/entropy/rans_symbol_decoder.h" - "${draco_src_root}/compression/entropy/rans_symbol_encoder.h" - "${draco_src_root}/compression/entropy/shannon_entropy.cc" - "${draco_src_root}/compression/entropy/shannon_entropy.h" - "${draco_src_root}/compression/entropy/symbol_decoding.cc" - "${draco_src_root}/compression/entropy/symbol_decoding.h" - "${draco_src_root}/compression/entropy/symbol_encoding.cc" - "${draco_src_root}/compression/entropy/symbol_encoding.h") +list( + APPEND draco_compression_entropy_sources + "${draco_src_root}/compression/entropy/ans.h" + "${draco_src_root}/compression/entropy/rans_symbol_coding.h" + "${draco_src_root}/compression/entropy/rans_symbol_decoder.h" + "${draco_src_root}/compression/entropy/rans_symbol_encoder.h" + "${draco_src_root}/compression/entropy/shannon_entropy.cc" + "${draco_src_root}/compression/entropy/shannon_entropy.h" + "${draco_src_root}/compression/entropy/symbol_decoding.cc" + "${draco_src_root}/compression/entropy/symbol_decoding.h" + "${draco_src_root}/compression/entropy/symbol_encoding.cc" + "${draco_src_root}/compression/entropy/symbol_encoding.h") -list(APPEND draco_core_sources - "${draco_src_root}/core/bit_utils.cc" - "${draco_src_root}/core/bit_utils.h" - "${draco_src_root}/core/bounding_box.cc" - "${draco_src_root}/core/bounding_box.h" - "${draco_src_root}/core/cycle_timer.cc" - "${draco_src_root}/core/cycle_timer.h" - "${draco_src_root}/core/data_buffer.cc" - "${draco_src_root}/core/data_buffer.h" - "${draco_src_root}/core/decoder_buffer.cc" - "${draco_src_root}/core/decoder_buffer.h" - "${draco_src_root}/core/divide.cc" - "${draco_src_root}/core/divide.h" - "${draco_src_root}/core/draco_index_type.h" - "${draco_src_root}/core/draco_index_type_vector.h" - "${draco_src_root}/core/draco_types.cc" - "${draco_src_root}/core/draco_types.h" - "${draco_src_root}/core/encoder_buffer.cc" - "${draco_src_root}/core/encoder_buffer.h" - "${draco_src_root}/core/hash_utils.cc" - "${draco_src_root}/core/hash_utils.h" - "${draco_src_root}/core/macros.h" - "${draco_src_root}/core/math_utils.h" - "${draco_src_root}/core/options.cc" - "${draco_src_root}/core/options.h" - "${draco_src_root}/core/quantization_utils.cc" - "${draco_src_root}/core/quantization_utils.h" - "${draco_src_root}/core/status.h" - "${draco_src_root}/core/status_or.h" - "${draco_src_root}/core/varint_decoding.h" - "${draco_src_root}/core/varint_encoding.h" - "${draco_src_root}/core/vector_d.h") +list( + APPEND draco_core_sources + "${draco_src_root}/core/bit_utils.cc" + "${draco_src_root}/core/bit_utils.h" + "${draco_src_root}/core/bounding_box.cc" + "${draco_src_root}/core/bounding_box.h" + "${draco_src_root}/core/constants.h" + "${draco_src_root}/core/cycle_timer.cc" + "${draco_src_root}/core/cycle_timer.h" + "${draco_src_root}/core/data_buffer.cc" + "${draco_src_root}/core/data_buffer.h" + "${draco_src_root}/core/decoder_buffer.cc" + "${draco_src_root}/core/decoder_buffer.h" + "${draco_src_root}/core/divide.cc" + "${draco_src_root}/core/divide.h" + "${draco_src_root}/core/draco_index_type.h" + "${draco_src_root}/core/draco_index_type_vector.h" + "${draco_src_root}/core/draco_types.cc" + "${draco_src_root}/core/draco_types.h" + "${draco_src_root}/core/draco_version.h" + "${draco_src_root}/core/encoder_buffer.cc" + "${draco_src_root}/core/encoder_buffer.h" + "${draco_src_root}/core/hash_utils.cc" + "${draco_src_root}/core/hash_utils.h" + "${draco_src_root}/core/macros.h" + "${draco_src_root}/core/math_utils.h" + "${draco_src_root}/core/options.cc" + "${draco_src_root}/core/options.h" + "${draco_src_root}/core/quantization_utils.cc" + "${draco_src_root}/core/quantization_utils.h" + "${draco_src_root}/core/status.h" + "${draco_src_root}/core/status_or.h" + "${draco_src_root}/core/varint_decoding.h" + "${draco_src_root}/core/varint_encoding.h" + "${draco_src_root}/core/vector_d.h") -list(APPEND draco_io_sources - "${draco_src_root}/io/file_reader_factory.cc" - "${draco_src_root}/io/file_reader_factory.h" - "${draco_src_root}/io/file_reader_interface.h" - "${draco_src_root}/io/file_utils.cc" - "${draco_src_root}/io/file_utils.h" - "${draco_src_root}/io/file_writer_factory.cc" - "${draco_src_root}/io/file_writer_factory.h" - "${draco_src_root}/io/file_writer_interface.h" - "${draco_src_root}/io/file_writer_utils.h" - "${draco_src_root}/io/file_writer_utils.cc" - "${draco_src_root}/io/mesh_io.cc" - "${draco_src_root}/io/mesh_io.h" - "${draco_src_root}/io/obj_decoder.cc" - "${draco_src_root}/io/obj_decoder.h" - "${draco_src_root}/io/obj_encoder.cc" - "${draco_src_root}/io/obj_encoder.h" - "${draco_src_root}/io/parser_utils.cc" - "${draco_src_root}/io/parser_utils.h" - "${draco_src_root}/io/ply_decoder.cc" - "${draco_src_root}/io/ply_decoder.h" - "${draco_src_root}/io/ply_encoder.cc" - "${draco_src_root}/io/ply_encoder.h" - "${draco_src_root}/io/ply_property_reader.h" - "${draco_src_root}/io/ply_property_writer.h" - "${draco_src_root}/io/ply_reader.cc" - "${draco_src_root}/io/ply_reader.h" - "${draco_src_root}/io/point_cloud_io.cc" - "${draco_src_root}/io/point_cloud_io.h" - "${draco_src_root}/io/stdio_file_reader.cc" - "${draco_src_root}/io/stdio_file_reader.h" - "${draco_src_root}/io/stdio_file_writer.cc" - "${draco_src_root}/io/stdio_file_writer.h") +list( + APPEND draco_io_sources + "${draco_src_root}/io/file_reader_factory.cc" + "${draco_src_root}/io/file_reader_factory.h" + "${draco_src_root}/io/file_reader_interface.h" + "${draco_src_root}/io/file_utils.cc" + "${draco_src_root}/io/file_utils.h" + "${draco_src_root}/io/file_writer_factory.cc" + "${draco_src_root}/io/file_writer_factory.h" + "${draco_src_root}/io/file_writer_interface.h" + "${draco_src_root}/io/file_writer_utils.h" + "${draco_src_root}/io/file_writer_utils.cc" + "${draco_src_root}/io/mesh_io.cc" + "${draco_src_root}/io/mesh_io.h" + "${draco_src_root}/io/obj_decoder.cc" + "${draco_src_root}/io/obj_decoder.h" + "${draco_src_root}/io/obj_encoder.cc" + "${draco_src_root}/io/obj_encoder.h" + "${draco_src_root}/io/parser_utils.cc" + "${draco_src_root}/io/parser_utils.h" + "${draco_src_root}/io/ply_decoder.cc" + "${draco_src_root}/io/ply_decoder.h" + "${draco_src_root}/io/ply_encoder.cc" + "${draco_src_root}/io/ply_encoder.h" + "${draco_src_root}/io/ply_property_reader.h" + "${draco_src_root}/io/ply_property_writer.h" + "${draco_src_root}/io/ply_reader.cc" + "${draco_src_root}/io/ply_reader.h" + "${draco_src_root}/io/stl_decoder.cc" + "${draco_src_root}/io/stl_decoder.h" + "${draco_src_root}/io/stl_encoder.cc" + "${draco_src_root}/io/stl_encoder.h" + "${draco_src_root}/io/point_cloud_io.cc" + "${draco_src_root}/io/point_cloud_io.h" + "${draco_src_root}/io/stdio_file_reader.cc" + "${draco_src_root}/io/stdio_file_reader.h" + "${draco_src_root}/io/stdio_file_writer.cc" + "${draco_src_root}/io/stdio_file_writer.h") -list(APPEND draco_mesh_sources - "${draco_src_root}/mesh/corner_table.cc" - "${draco_src_root}/mesh/corner_table.h" - "${draco_src_root}/mesh/corner_table_iterators.h" - "${draco_src_root}/mesh/mesh.cc" - "${draco_src_root}/mesh/mesh.h" - "${draco_src_root}/mesh/mesh_are_equivalent.cc" - "${draco_src_root}/mesh/mesh_are_equivalent.h" - "${draco_src_root}/mesh/mesh_attribute_corner_table.cc" - "${draco_src_root}/mesh/mesh_attribute_corner_table.h" - "${draco_src_root}/mesh/mesh_cleanup.cc" - "${draco_src_root}/mesh/mesh_cleanup.h" - "${draco_src_root}/mesh/mesh_misc_functions.cc" - "${draco_src_root}/mesh/mesh_misc_functions.h" - "${draco_src_root}/mesh/mesh_stripifier.cc" - "${draco_src_root}/mesh/mesh_stripifier.h" - "${draco_src_root}/mesh/triangle_soup_mesh_builder.cc" - "${draco_src_root}/mesh/triangle_soup_mesh_builder.h" - "${draco_src_root}/mesh/valence_cache.h") +list( + APPEND draco_mesh_sources + "${draco_src_root}/mesh/corner_table.cc" + "${draco_src_root}/mesh/corner_table.h" + "${draco_src_root}/mesh/corner_table_iterators.h" + "${draco_src_root}/mesh/mesh.cc" + "${draco_src_root}/mesh/mesh.h" + "${draco_src_root}/mesh/mesh_are_equivalent.cc" + "${draco_src_root}/mesh/mesh_are_equivalent.h" + "${draco_src_root}/mesh/mesh_attribute_corner_table.cc" + "${draco_src_root}/mesh/mesh_attribute_corner_table.h" + "${draco_src_root}/mesh/mesh_cleanup.cc" + "${draco_src_root}/mesh/mesh_cleanup.h" + "${draco_src_root}/mesh/mesh_features.cc" + "${draco_src_root}/mesh/mesh_features.h" + "${draco_src_root}/mesh/mesh_indices.h" + "${draco_src_root}/mesh/mesh_misc_functions.cc" + "${draco_src_root}/mesh/mesh_misc_functions.h" + "${draco_src_root}/mesh/mesh_stripifier.cc" + "${draco_src_root}/mesh/mesh_stripifier.h" + "${draco_src_root}/mesh/triangle_soup_mesh_builder.cc" + "${draco_src_root}/mesh/triangle_soup_mesh_builder.h" + "${draco_src_root}/mesh/valence_cache.h") -list(APPEND draco_point_cloud_sources - "${draco_src_root}/point_cloud/point_cloud.cc" - "${draco_src_root}/point_cloud/point_cloud.h" - "${draco_src_root}/point_cloud/point_cloud_builder.cc" - "${draco_src_root}/point_cloud/point_cloud_builder.h") +list( + APPEND draco_point_cloud_sources + "${draco_src_root}/point_cloud/point_cloud.cc" + "${draco_src_root}/point_cloud/point_cloud.h" + "${draco_src_root}/point_cloud/point_cloud_builder.cc" + "${draco_src_root}/point_cloud/point_cloud_builder.h") list( APPEND @@ -424,7 +462,7 @@ list( "${draco_src_root}/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_decoder.cc" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_decoder.h" - ) +) list( APPEND @@ -433,13 +471,18 @@ list( "${draco_src_root}/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_encoder.cc" "${draco_src_root}/compression/point_cloud/algorithms/float_points_tree_encoder.h" - ) +) -list(APPEND draco_metadata_sources - "${draco_src_root}/metadata/geometry_metadata.cc" - "${draco_src_root}/metadata/geometry_metadata.h" - "${draco_src_root}/metadata/metadata.cc" - "${draco_src_root}/metadata/metadata.h") +list( + APPEND draco_metadata_sources + "${draco_src_root}/metadata/geometry_metadata.cc" + "${draco_src_root}/metadata/geometry_metadata.h" + "${draco_src_root}/metadata/metadata.cc" + "${draco_src_root}/metadata/metadata.h" + "${draco_src_root}/metadata/property_table.cc" + "${draco_src_root}/metadata/property_table.h" + "${draco_src_root}/metadata/structural_metadata.cc" + "${draco_src_root}/metadata/structural_metadata.h") list(APPEND draco_metadata_enc_sources "${draco_src_root}/metadata/metadata_encoder.cc" @@ -465,7 +508,7 @@ list( APPEND draco_js_dec_sources "${draco_src_root}/javascript/emscripten/decoder_webidl_wrapper.cc" "${draco_src_root}/javascript/emscripten/draco_decoder_glue_wrapper.cc" - ) +) list( APPEND draco_js_enc_sources @@ -477,14 +520,14 @@ list( draco_animation_js_dec_sources "${draco_src_root}/javascript/emscripten/animation_decoder_webidl_wrapper.cc" "${draco_src_root}/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc" - ) +) list( APPEND draco_animation_js_enc_sources "${draco_src_root}/javascript/emscripten/animation_encoder_webidl_wrapper.cc" "${draco_src_root}/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc" - ) +) list(APPEND draco_unity_plug_sources "${draco_src_root}/unity/draco_unity_plugin.cc" @@ -494,49 +537,133 @@ list(APPEND draco_maya_plug_sources "${draco_src_root}/maya/draco_maya_plugin.cc" "${draco_src_root}/maya/draco_maya_plugin.h") +if(DRACO_TRANSCODER_SUPPORTED) + list( + APPEND draco_animation_sources + "${draco_src_root}/animation/animation.cc" + "${draco_src_root}/animation/animation.h" + "${draco_src_root}/animation/node_animation_data.h" + "${draco_src_root}/animation/skin.cc" + "${draco_src_root}/animation/skin.h") + + list( + APPEND draco_io_sources + "${draco_src_root}/io/gltf_decoder.cc" + "${draco_src_root}/io/gltf_decoder.h" + "${draco_src_root}/io/gltf_encoder.cc" + "${draco_src_root}/io/gltf_encoder.h" + "${draco_src_root}/io/gltf_utils.cc" + "${draco_src_root}/io/gltf_utils.h" + "${draco_src_root}/io/image_compression_options.h" + "${draco_src_root}/io/scene_io.cc" + "${draco_src_root}/io/scene_io.h" + "${draco_src_root}/io/texture_io.cc" + "${draco_src_root}/io/texture_io.h" + "${draco_src_root}/io/tiny_gltf_utils.cc" + "${draco_src_root}/io/tiny_gltf_utils.h") + + list( + APPEND draco_material_sources + "${draco_src_root}/material/material.cc" + "${draco_src_root}/material/material.h" + "${draco_src_root}/material/material_library.cc" + "${draco_src_root}/material/material_library.h") + + list( + APPEND draco_mesh_sources + "${draco_src_root}/mesh/mesh_connected_components.h" + "${draco_src_root}/mesh/mesh_splitter.cc" + "${draco_src_root}/mesh/mesh_splitter.h" + "${draco_src_root}/mesh/mesh_utils.cc" + "${draco_src_root}/mesh/mesh_utils.h") + + list( + APPEND draco_scene_sources + "${draco_src_root}/scene/instance_array.cc" + "${draco_src_root}/scene/instance_array.h" + "${draco_src_root}/scene/light.cc" + "${draco_src_root}/scene/light.h" + "${draco_src_root}/scene/mesh_group.h" + "${draco_src_root}/scene/scene.cc" + "${draco_src_root}/scene/scene.h" + "${draco_src_root}/scene/scene_are_equivalent.cc" + "${draco_src_root}/scene/scene_are_equivalent.h" + "${draco_src_root}/scene/scene_indices.h" + "${draco_src_root}/scene/scene_node.h" + "${draco_src_root}/scene/scene_utils.cc" + "${draco_src_root}/scene/scene_utils.h" + "${draco_src_root}/scene/trs_matrix.cc" + "${draco_src_root}/scene/trs_matrix.h") + + list( + APPEND draco_texture_sources + "${draco_src_root}/texture/source_image.cc" + "${draco_src_root}/texture/source_image.h" + "${draco_src_root}/texture/texture.h" + "${draco_src_root}/texture/texture_library.cc" + "${draco_src_root}/texture/texture_library.h" + "${draco_src_root}/texture/texture_map.cc" + "${draco_src_root}/texture/texture_map.h" + "${draco_src_root}/texture/texture_transform.cc" + "${draco_src_root}/texture/texture_transform.h" + "${draco_src_root}/texture/texture_utils.cc" + "${draco_src_root}/texture/texture_utils.h") + + +endif() + # # Draco targets. # if(EMSCRIPTEN AND DRACO_JS_GLUE) # Draco decoder and encoder "executable" targets in various flavors for - # Emsscripten. - list(APPEND draco_decoder_src - ${draco_attributes_sources} - ${draco_compression_attributes_dec_sources} - ${draco_compression_attributes_pred_schemes_dec_sources} - ${draco_compression_bit_coders_sources} - ${draco_compression_decode_sources} - ${draco_compression_entropy_sources} - ${draco_compression_mesh_traverser_sources} - ${draco_compression_mesh_dec_sources} - ${draco_compression_point_cloud_dec_sources} - ${draco_core_sources} - ${draco_dec_config_sources} - ${draco_js_dec_sources} - ${draco_mesh_sources} - ${draco_metadata_dec_sources} - ${draco_metadata_sources} - ${draco_point_cloud_sources} - ${draco_points_dec_sources}) + # Emscripten. - list(APPEND draco_encoder_src - ${draco_attributes_sources} - ${draco_compression_attributes_enc_sources} - ${draco_compression_attributes_pred_schemes_enc_sources} - ${draco_compression_bit_coders_sources} - ${draco_compression_encode_sources} - ${draco_compression_entropy_sources} - ${draco_compression_mesh_traverser_sources} - ${draco_compression_mesh_enc_sources} - ${draco_compression_point_cloud_enc_sources} - ${draco_core_sources} - ${draco_enc_config_sources} - ${draco_js_enc_sources} - ${draco_mesh_sources} - ${draco_metadata_enc_sources} - ${draco_metadata_sources} - ${draco_point_cloud_sources} - ${draco_points_enc_sources}) + if(DRACO_TRANSCODER_SUPPORTED) + message(FATAL_ERROR "The transcoder is not supported in Emscripten.") + endif() + + list( + APPEND draco_decoder_src + ${draco_attributes_sources} + ${draco_compression_attributes_dec_sources} + ${draco_compression_attributes_pred_schemes_dec_sources} + ${draco_compression_bit_coders_sources} + ${draco_compression_decode_sources} + ${draco_compression_entropy_sources} + ${draco_compression_mesh_traverser_sources} + ${draco_compression_mesh_dec_sources} + ${draco_compression_options_sources} + ${draco_compression_point_cloud_dec_sources} + ${draco_core_sources} + ${draco_dec_config_sources} + ${draco_js_dec_sources} + ${draco_mesh_sources} + ${draco_metadata_dec_sources} + ${draco_metadata_sources} + ${draco_point_cloud_sources} + ${draco_points_dec_sources}) + + list( + APPEND draco_encoder_src + ${draco_attributes_sources} + ${draco_compression_attributes_enc_sources} + ${draco_compression_attributes_pred_schemes_enc_sources} + ${draco_compression_bit_coders_sources} + ${draco_compression_encode_sources} + ${draco_compression_entropy_sources} + ${draco_compression_mesh_traverser_sources} + ${draco_compression_mesh_enc_sources} + ${draco_compression_options_sources} + ${draco_compression_point_cloud_enc_sources} + ${draco_core_sources} + ${draco_enc_config_sources} + ${draco_js_enc_sources} + ${draco_mesh_sources} + ${draco_metadata_enc_sources} + ${draco_metadata_sources} + ${draco_point_cloud_sources} + ${draco_points_enc_sources}) list(APPEND draco_js_dec_idl "${draco_src_root}/javascript/emscripten/draco_web_decoder.idl") @@ -561,10 +688,10 @@ if(EMSCRIPTEN AND DRACO_JS_GLUE) set(draco_decoder_glue_path "${draco_build}/glue_decoder") set(draco_encoder_glue_path "${draco_build}/glue_encoder") - draco_generate_emscripten_glue(INPUT_IDL ${draco_js_dec_idl} OUTPUT_PATH - ${draco_decoder_glue_path}) - draco_generate_emscripten_glue(INPUT_IDL ${draco_js_enc_idl} OUTPUT_PATH - ${draco_encoder_glue_path}) + draco_generate_emscripten_glue(INPUT_IDL ${draco_js_dec_idl} + OUTPUT_PATH ${draco_decoder_glue_path}) + draco_generate_emscripten_glue(INPUT_IDL ${draco_js_enc_idl} + OUTPUT_PATH ${draco_encoder_glue_path}) if(DRACO_DECODER_ATTRIBUTE_DEDUPLICATION) list(APPEND draco_decoder_features @@ -572,45 +699,28 @@ if(EMSCRIPTEN AND DRACO_JS_GLUE) "DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED") endif() - draco_add_emscripten_executable(NAME - draco_decoder - SOURCES - ${draco_decoder_src} - DEFINES - ${draco_defines} - FEATURES - ${draco_decoder_features} - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoDecoderModule\"" - GLUE_PATH - ${draco_decoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_decoder_sources}) + draco_add_emscripten_executable( + NAME draco_decoder + SOURCES ${draco_decoder_src} + DEFINES ${draco_defines} + FEATURES ${draco_decoder_features} + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoDecoderModule\"" + GLUE_PATH ${draco_decoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_decoder_sources}) draco_add_emscripten_executable( - NAME - draco_encoder - SOURCES - ${draco_encoder_src} - DEFINES - ${draco_defines} - FEATURES - DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED - DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoEncoderModule\"" - GLUE_PATH - ${draco_encoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_sources}) + NAME draco_encoder + SOURCES ${draco_encoder_src} + DEFINES ${draco_defines} + FEATURES DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED + DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoEncoderModule\"" + GLUE_PATH ${draco_encoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_sources}) if(DRACO_ANIMATION_ENCODING) set(draco_anim_decoder_glue_path "${draco_build}/glue_animation_decoder") @@ -622,186 +732,270 @@ if(EMSCRIPTEN AND DRACO_JS_GLUE) OUTPUT_PATH ${draco_anim_encoder_glue_path}) draco_add_emscripten_executable( - NAME - draco_animation_decoder - SOURCES - ${draco_animation_dec_sources} - ${draco_animation_js_dec_sources} - ${draco_animation_sources} - ${draco_decoder_src} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoAnimationDecoderModule\"" - GLUE_PATH - ${draco_anim_decoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_decoder_sources}) + NAME draco_animation_decoder + SOURCES ${draco_animation_dec_sources} ${draco_animation_js_dec_sources} + ${draco_animation_sources} ${draco_decoder_src} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoAnimationDecoderModule\"" + GLUE_PATH ${draco_anim_decoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_decoder_sources}) draco_add_emscripten_executable( - NAME - draco_animation_encoder - SOURCES - ${draco_animation_enc_sources} - ${draco_animation_js_enc_sources} - ${draco_animation_sources} - ${draco_encoder_src} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LINK_FLAGS - "-sEXPORT_NAME=\"DracoAnimationEncoderModule\"" - GLUE_PATH - ${draco_anim_encoder_glue_path} - PRE_LINK_JS_SOURCES - ${draco_pre_link_js_sources} - POST_LINK_JS_SOURCES - ${draco_post_link_js_sources}) + NAME draco_animation_encoder + SOURCES ${draco_animation_enc_sources} ${draco_animation_js_enc_sources} + ${draco_animation_sources} ${draco_encoder_src} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LINK_FLAGS "-sEXPORT_NAME=\"DracoAnimationEncoderModule\"" + GLUE_PATH ${draco_anim_encoder_glue_path} + PRE_LINK_JS_SOURCES ${draco_pre_link_js_sources} + POST_LINK_JS_SOURCES ${draco_post_link_js_sources}) endif() else() # Standard Draco libs, encoder and decoder. Object collections that mirror the # Draco directory structure. - draco_add_library(NAME draco_attributes TYPE OBJECT SOURCES - ${draco_attributes_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME - draco_compression_attributes_dec - OBJECT - ${draco_compression_attributes_dec_sources} - TYPE - OBJECT - SOURCES - ${draco_compression_attributes_dec_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths}) - draco_add_library(NAME draco_compression_attributes_enc TYPE OBJECT SOURCES - ${draco_compression_attributes_enc_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_attributes_pred_schemes_dec TYPE - OBJECT SOURCES - ${draco_compression_attributes_pred_schemes_dec_sources}) - draco_add_library(NAME draco_compression_attributes_pred_schemes_enc TYPE - OBJECT SOURCES - ${draco_compression_attributes_pred_schemes_enc_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_bit_coders TYPE OBJECT SOURCES - ${draco_compression_bit_coders_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_enc_config TYPE OBJECT SOURCES - ${draco_enc_config_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_dec_config TYPE OBJECT SOURCES - ${draco_dec_config_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_decode TYPE OBJECT SOURCES - ${draco_compression_decode_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_encode TYPE OBJECT SOURCES - ${draco_compression_encode_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_entropy TYPE OBJECT SOURCES - ${draco_compression_entropy_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_mesh_traverser TYPE OBJECT SOURCES - ${draco_compression_mesh_traverser_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_mesh_dec TYPE OBJECT SOURCES - ${draco_compression_mesh_dec_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_mesh_enc TYPE OBJECT SOURCES - ${draco_compression_mesh_enc_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_point_cloud_dec TYPE OBJECT SOURCES - ${draco_compression_point_cloud_dec_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_compression_point_cloud_enc TYPE OBJECT SOURCES - ${draco_compression_point_cloud_enc_sources} DEFINES - ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_core TYPE OBJECT SOURCES ${draco_core_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_io TYPE OBJECT SOURCES ${draco_io_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_mesh TYPE OBJECT SOURCES ${draco_mesh_sources} - DEFINES ${draco_defines} INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_metadata_dec TYPE OBJECT SOURCES - ${draco_metadata_dec_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_metadata_enc TYPE OBJECT SOURCES - ${draco_metadata_enc_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_metadata TYPE OBJECT SOURCES - ${draco_metadata_sources} DEFINES ${draco_defines} INCLUDES - ${draco_include_paths}) - draco_add_library(NAME draco_animation_dec TYPE OBJECT SOURCES - ${draco_animation_dec_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_animation_enc TYPE OBJECT SOURCES - ${draco_animation_enc_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME draco_animation TYPE OBJECT SOURCES - ${draco_animation_sources} DEFINES ${draco_defines} INCLUDES - ${draco_include_paths}) - draco_add_library(NAME draco_point_cloud TYPE OBJECT SOURCES - ${draco_point_cloud_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) - draco_add_library(NAME - draco_points_dec - TYPE - OBJECT - SOURCES - ${draco_points_common_sources} - ${draco_points_dec_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths}) - draco_add_library(NAME - draco_points_enc - TYPE - OBJECT - SOURCES - ${draco_points_common_sources} - ${draco_points_enc_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths}) + draco_add_library( + NAME draco_attributes + TYPE OBJECT + SOURCES ${draco_attributes_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_attributes_dec OBJECT + ${draco_compression_attributes_dec_sources} + TYPE OBJECT + SOURCES ${draco_compression_attributes_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_attributes_enc + TYPE OBJECT + SOURCES ${draco_compression_attributes_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_attributes_pred_schemes_dec + TYPE OBJECT + SOURCES ${draco_compression_attributes_pred_schemes_dec_sources}) + draco_add_library( + NAME draco_compression_attributes_pred_schemes_enc + TYPE OBJECT + SOURCES ${draco_compression_attributes_pred_schemes_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_bit_coders + TYPE OBJECT + SOURCES ${draco_compression_bit_coders_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_enc_config + TYPE OBJECT + SOURCES ${draco_enc_config_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_dec_config + TYPE OBJECT + SOURCES ${draco_dec_config_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_decode + TYPE OBJECT + SOURCES ${draco_compression_decode_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_encode + TYPE OBJECT + SOURCES ${draco_compression_encode_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_entropy + TYPE OBJECT + SOURCES ${draco_compression_entropy_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_mesh_traverser + TYPE OBJECT + SOURCES ${draco_compression_mesh_traverser_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_mesh_dec + TYPE OBJECT + SOURCES ${draco_compression_mesh_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_mesh_enc + TYPE OBJECT + SOURCES ${draco_compression_mesh_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_options + TYPE OBJECT + SOURCES ${draco_compression_options_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_point_cloud_dec + TYPE OBJECT + SOURCES ${draco_compression_point_cloud_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_compression_point_cloud_enc + TYPE OBJECT + SOURCES ${draco_compression_point_cloud_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_core + TYPE OBJECT + SOURCES ${draco_core_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_io + TYPE OBJECT + SOURCES ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_mesh + TYPE OBJECT + SOURCES ${draco_mesh_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_metadata_dec + TYPE OBJECT + SOURCES ${draco_metadata_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_metadata_enc + TYPE OBJECT + SOURCES ${draco_metadata_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_metadata + TYPE OBJECT + SOURCES ${draco_metadata_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_animation_dec + TYPE OBJECT + SOURCES ${draco_animation_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_animation_enc + TYPE OBJECT + SOURCES ${draco_animation_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_animation + TYPE OBJECT + SOURCES ${draco_animation_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_point_cloud + TYPE OBJECT + SOURCES ${draco_point_cloud_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_points_dec + TYPE OBJECT + SOURCES ${draco_points_common_sources} ${draco_points_dec_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_points_enc + TYPE OBJECT + SOURCES ${draco_points_common_sources} ${draco_points_enc_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) - set(draco_object_library_deps - draco_attributes - draco_compression_attributes_dec - draco_compression_attributes_enc - draco_compression_attributes_pred_schemes_dec - draco_compression_attributes_pred_schemes_enc - draco_compression_bit_coders - draco_compression_decode - draco_compression_encode - draco_compression_entropy - draco_compression_mesh_dec - draco_compression_mesh_enc - draco_compression_point_cloud_dec - draco_compression_point_cloud_enc - draco_core - draco_dec_config - draco_enc_config - draco_io - draco_mesh - draco_metadata - draco_metadata_dec - draco_metadata_enc - draco_animation - draco_animation_dec - draco_animation_enc - draco_point_cloud - draco_points_dec - draco_points_enc) + if(DRACO_TRANSCODER_SUPPORTED) + if(MSVC) + # TODO(https://github.com/google/draco/issues/826) + set_source_files_properties("${draco_src_root}/io/gltf_decoder.cc" + PROPERTIES COMPILE_OPTIONS "/Od") + endif() + + draco_add_library( + NAME draco_material + TYPE OBJECT + SOURCES ${draco_material_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + + draco_add_library( + NAME draco_scene + TYPE OBJECT + SOURCES ${draco_scene_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + + draco_add_library( + NAME draco_texture + TYPE OBJECT + SOURCES ${draco_texture_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) + + endif() + + list( + APPEND draco_object_library_deps + draco_attributes + draco_compression_attributes_dec + draco_compression_attributes_enc + draco_compression_attributes_pred_schemes_dec + draco_compression_attributes_pred_schemes_enc + draco_compression_bit_coders + draco_compression_decode + draco_compression_encode + draco_compression_entropy + draco_compression_mesh_dec + draco_compression_mesh_enc + draco_compression_options + draco_compression_point_cloud_dec + draco_compression_point_cloud_enc + draco_core + draco_dec_config + draco_enc_config + draco_io + draco_mesh + draco_metadata + draco_metadata_dec + draco_metadata_enc + draco_animation + draco_animation_dec + draco_animation_enc + draco_point_cloud + draco_points_dec + draco_points_enc) + + if(DRACO_TRANSCODER_SUPPORTED) + list(APPEND draco_object_library_deps draco_material draco_scene + draco_texture) + + endif() # Library targets that consume the object collections. if(MSVC) @@ -809,56 +1003,48 @@ else() # that the exported symbols are part of the DLL target. The unfortunate side # effect of this is that a single configuration cannot output both the # static library and the DLL: This results in an either/or situation. - # Windows users of the draco build can have a DLL and an import library, - # or they can have a static library; they cannot have both from a single + # Windows users of the draco build can have a DLL and an import library, or + # they can have a static library; they cannot have both from a single # configuration of the build. if(BUILD_SHARED_LIBS) set(draco_lib_type SHARED) else() set(draco_lib_type STATIC) endif() - draco_add_library(NAME - draco - OUTPUT_NAME - draco - TYPE - ${draco_lib_type} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - ${draco_object_library_deps}) + draco_add_library( + NAME draco + OUTPUT_NAME draco + TYPE ${draco_lib_type} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS ${draco_object_library_deps} + LIB_DEPS ${draco_lib_deps}) + add_library(draco::draco ALIAS draco) else() - draco_add_library(NAME - draco_static - OUTPUT_NAME - draco - TYPE - STATIC - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - ${draco_object_library_deps}) + draco_add_library( + NAME draco_static + OUTPUT_NAME draco + TYPE STATIC + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS ${draco_object_library_deps} + LIB_DEPS ${draco_lib_deps}) if(BUILD_SHARED_LIBS) - draco_add_library(NAME - draco_shared - SOURCES - "${draco_src_root}/core/draco_version.h" - OUTPUT_NAME - draco - TYPE - SHARED - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LIB_DEPS - draco_static) + draco_add_library( + NAME draco_shared + SOURCES "${draco_src_root}/core/draco_version.h" + OUTPUT_NAME draco + TYPE SHARED + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS draco_static) + add_library(draco::draco ALIAS draco_shared) + set_target_properties(draco_shared PROPERTIES EXPORT_NAME draco) + else() + add_library(draco::draco ALIAS draco_static) + set_target_properties(draco_static PROPERTIES EXPORT_NAME draco) endif() endif() @@ -869,22 +1055,20 @@ else() set(unity_decoder_lib_type MODULE) endif() - draco_add_library(NAME draco_unity_plugin TYPE OBJECT SOURCES - ${draco_unity_plug_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_unity_plugin + TYPE OBJECT + SOURCES ${draco_unity_plug_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) - draco_add_library(NAME - dracodec_unity - TYPE - ${unity_decoder_lib_type} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - draco_unity_plugin - LIB_DEPS - ${draco_plugin_dependency}) + draco_add_library( + NAME dracodec_unity + TYPE ${unity_decoder_lib_type} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS draco_unity_plugin + LIB_DEPS ${draco_plugin_dependency}) # For Mac, we need to build a .bundle for the unity plugin. if(APPLE) @@ -893,22 +1077,20 @@ else() endif() if(DRACO_MAYA_PLUGIN) - draco_add_library(NAME draco_maya_plugin TYPE OBJECT SOURCES - ${draco_maya_plug_sources} DEFINES ${draco_defines} - INCLUDES ${draco_include_paths}) + draco_add_library( + NAME draco_maya_plugin + TYPE OBJECT + SOURCES ${draco_maya_plug_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths}) - draco_add_library(NAME - draco_maya_wrapper - TYPE - MODULE - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - OBJLIB_DEPS - draco_maya_plugin - LIB_DEPS - ${draco_plugin_dependency}) + draco_add_library( + NAME draco_maya_wrapper + TYPE MODULE + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + OBJLIB_DEPS draco_maya_plugin + LIB_DEPS ${draco_plugin_dependency}) # For Mac, we need to build a .bundle for the plugin. if(APPLE) @@ -917,29 +1099,44 @@ else() endif() # Draco app targets. - draco_add_executable(NAME - draco_decoder - SOURCES - "${draco_src_root}/tools/draco_decoder.cc" - ${draco_io_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LIB_DEPS - ${draco_dependency}) + draco_add_executable( + NAME draco_decoder + SOURCES "${draco_src_root}/tools/draco_decoder.cc" ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) - draco_add_executable(NAME - draco_encoder - SOURCES - "${draco_src_root}/tools/draco_encoder.cc" - ${draco_io_sources} - DEFINES - ${draco_defines} - INCLUDES - ${draco_include_paths} - LIB_DEPS - ${draco_dependency}) + draco_add_executable( + NAME draco_encoder + SOURCES "${draco_src_root}/tools/draco_encoder.cc" ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) + + if(DRACO_TRANSCODER_SUPPORTED) + draco_add_executable( + NAME draco_transcoder + SOURCES "${draco_src_root}/tools/draco_transcoder.cc" + "${draco_src_root}/tools/draco_transcoder_lib.cc" + "${draco_src_root}/tools/draco_transcoder_lib.h" + ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) + + if(DRACO_SIMPLIFIER_SUPPORTED) + draco_add_executable( + NAME draco_simplifier + SOURCES ${draco_pipeline_proto_header} + "${draco_src_root}/tools/draco_simplifier.cc" + "${draco_src_root}/tools/draco_simplifier_lib.cc" + "${draco_src_root}/tools/draco_simplifier_lib.h" + ${draco_io_sources} + DEFINES ${draco_defines} + INCLUDES ${draco_include_paths} + LIB_DEPS ${draco_dependency}) + endif() + endif() draco_setup_install_target() draco_setup_test_targets() diff --git a/contrib/draco/README.md b/contrib/draco/README.md index 0d980b387..4cc717c8d 100644 --- a/contrib/draco/README.md +++ b/contrib/draco/README.md @@ -2,10 +2,93 @@

-[![Build Status](https://github.com/google/draco/workflows/Build/badge.svg)](https://github.com/google/draco/actions?query=workflow%3ABuild) +[![draco-ci](https://github.com/google/draco/workflows/draco-ci/badge.svg?branch=master)](https://github.com/google/draco/actions/workflows/ci.yml) News ======= + +Attention GStatic users: the Draco team strongly recommends using the versioned +URLs for accessing Draco GStatic content. If you are using the URLs that include +the `v1/decoders` substring within the URL, edge caching and GStatic propagation +delays can result in transient errors that can be difficult to diagnose when +new Draco releases are launched. To avoid the issue pin your sites to a +versioned release. + +### Version 1.5.6 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.6, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.6/* +* The CMake flag DRACO_DEBUG_MSVC_WARNINGS has been replaced with + DRACO_DEBUG_COMPILER_WARNINGS, and the behavior has changed. It is now a + boolean flag defined in draco_options.cmake. +* Bug fixes. +* Security fixes. + +### Version 1.5.5 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.5, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.5/* +* Bug fix: https://github.com/google/draco/issues/935 + +### Version 1.5.4 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.4, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.4/* +* Added partial support for glTF extensions EXT_mesh_features and + EXT_structural_metadata. +* Bug fixes. +* Security fixes. + +### Version 1.5.3 release: +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.5.3, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.5.3/* +* Bug fixes. + +### Version 1.5.2 release +* This is the same as v1.5.1 with the following two bug fixes: + * Fixes DRACO_TRANSCODER_SUPPORTED enabled builds. + * ABI version updated. + +### Version 1.5.1 release +* Adds assertion enabled Emscripten builds to the release, and a subset of the + assertion enabled builds to GStatic. See the file listing below. +* Custom paths to third party dependencies are now supported. See BUILDING.md + for more information. +* The CMake configuration file draco-config.cmake is now tested and known to + work for using Draco in Linux, MacOS, and Windows CMake projects. See the + `install_test` subdirectory of `src/draco/tools` for more information. +* Bug fixes. + +### Version 1.5.0 release +* Adds the draco_transcoder tool. See the section below on the glTF transcoding + tool, and BUILDING.md for build and dependency information. +* Some changes to configuration variables have been made for this release: + - The DRACO_GLTF flag has been renamed to DRACO_GLTF_BITSTREAM to help + increase understanding of its purpose, which is to limit Draco features to + those included in the Draco glTF specification. + - Variables exported in CMake via draco-config.cmake and find-draco.cmake + (formerly FindDraco.cmake) have been renamed. It's unlikely that this + impacts any existing projects as the aforementioned files were not formed + correctly. See [PR775](https://github.com/google/draco/pull/775) for full + details of the changes. +* A CMake version file has been added. +* The CMake install target now uses absolute paths direct from CMake instead + of building them using CMAKE_INSTALL_PREFIX. This was done to make Draco + easier to use for downstream packagers and should have little to no impact on + users picking up Draco from source. +* Certain MSVC warnings have had their levels changed via compiler flag to + reduce the amount of noise output by the MSVC compilers. Set MSVC warning + level to 4, or define DRACO_DEBUG_MSVC_WARNINGS at CMake configuration time + to restore previous behavior. +* Bug fixes. + +### Version 1.4.3 release +* Using the versioned www.gstatic.com WASM and Javascript decoders continues + to be recommended. To use v1.4.3, use this URL: + * https://www.gstatic.com/draco/versioned/decoders/1.4.3/* +* Bug fixes + ### Version 1.4.1 release * Using the versioned www.gstatic.com WASM and Javascript decoders is now recommended. To use v1.4.1, use this URL: @@ -129,6 +212,7 @@ _**Contents**_ * [Encoding Tool](#encoding-tool) * [Encoding Point Clouds](#encoding-point-clouds) * [Decoding Tool](#decoding-tool) + * [glTF Transcoding Tool](#gltf-transcoding-tool) * [C++ Decoder API](#c-decoder-api) * [Javascript Encoder API](#javascript-encoder-api) * [Javascript Decoder API](#javascript-decoder-api) @@ -136,6 +220,7 @@ _**Contents**_ * [Metadata API](#metadata-api) * [NPM Package](#npm-package) * [three.js Renderer Example](#threejs-renderer-example) + * [GStatic Javascript Builds](#gstatic-javascript-builds) * [Support](#support) * [License](#license) * [References](#references) @@ -170,16 +255,18 @@ Command Line Applications ------------------------ The default target created from the build files will be the `draco_encoder` -and `draco_decoder` command line applications. For both applications, if you -run them without any arguments or `-h`, the applications will output usage and -options. +and `draco_decoder` command line applications. Additionally, `draco_transcoder` +is generated when CMake is run with the DRACO_TRANSCODER_SUPPORTED variable set +to ON (see [BUILDING](BUILDING.md#transcoder) for more details). For all +applications, if you run them without any arguments or `-h`, the applications +will output usage and options. Encoding Tool ------------- -`draco_encoder` will read OBJ or PLY files as input, and output Draco-encoded -files. We have included Stanford's [Bunny] mesh for testing. The basic command -line looks like this: +`draco_encoder` will read OBJ, STL or PLY files as input, and output +Draco-encoded files. We have included Stanford's [Bunny] mesh for testing. The +basic command line looks like this: ~~~~~ bash ./draco_encoder -i testdata/bun_zipper.ply -o out.drc @@ -232,15 +319,34 @@ and denser point clouds. Decoding Tool ------------- -`draco_decoder` will read Draco files as input, and output OBJ or PLY files. -The basic command line looks like this: +`draco_decoder` will read Draco files as input, and output OBJ, STL or PLY +files. The basic command line looks like this: ~~~~~ bash ./draco_decoder -i in.drc -o out.obj ~~~~~ +glTF Transcoding Tool +--------------------- + +`draco_transcoder` can be used to add Draco compression to glTF assets. The +basic command line looks like this: + +~~~~~ bash +./draco_transcoder -i in.glb -o out.glb +~~~~~ + +This command line will add geometry compression to all meshes in the `in.glb` +file. Quantization values for different glTF attributes can be specified +similarly to the `draco_encoder` tool. For example `-qp` can be used to define +quantization of the position attribute: + +~~~~~ bash +./draco_transcoder -i in.glb -o out.glb -qp 12 +~~~~~ + C++ Decoder API -------------- +--------------- If you'd like to add decoding to your applications you will need to include the `draco_dec` library. In order to use the Draco decoder you need to @@ -442,6 +548,30 @@ Javascript decoder using the `three.js` renderer. Please see the [javascript/example/README.md](javascript/example/README.md) file for more information. +GStatic Javascript Builds +========================= + +Prebuilt versions of the Emscripten-built Draco javascript decoders are hosted +on www.gstatic.com in version labeled directories: + +https://www.gstatic.com/draco/versioned/decoders/VERSION/* + +As of the v1.4.3 release the files available are: + +- [draco_decoder.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder.js) +- [draco_decoder.wasm](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder.wasm) +- [draco_decoder_gltf.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder_gltf.js) +- [draco_decoder_gltf.wasm](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_decoder_gltf.wasm) +- [draco_wasm_wrapper.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_wasm_wrapper.js) +- [draco_wasm_wrapper_gltf.js](https://www.gstatic.com/draco/versioned/decoders/1.4.3/draco_wasm_wrapper_gltf.js) + +Beginning with the v1.5.1 release assertion enabled builds of the following +files are available: + +- [draco_decoder.js](https://www.gstatic.com/draco/versioned/decoders/1.5.1/with_asserts/draco_decoder.js) +- [draco_decoder.wasm](https://www.gstatic.com/draco/versioned/decoders/1.5.1/with_asserts/draco_decoder.wasm) +- [draco_wasm_wrapper.js](https://www.gstatic.com/draco/versioned/decoders/1.5.1/with_asserts/draco_wasm_wrapper.js) + Support ======= diff --git a/contrib/draco/cmake/DracoConfig.cmake b/contrib/draco/cmake/DracoConfig.cmake deleted file mode 100644 index be5e1faef..000000000 --- a/contrib/draco/cmake/DracoConfig.cmake +++ /dev/null @@ -1,3 +0,0 @@ -@PACKAGE_INIT@ -set_and_check(draco_INCLUDE_DIR "@PACKAGE_draco_include_install_dir@") -set_and_check(draco_LIBRARY_DIR "@PACKAGE_draco_lib_install_dir@") diff --git a/contrib/draco/cmake/FindDraco.cmake b/contrib/draco/cmake/FindDraco.cmake deleted file mode 100644 index 0a9193065..000000000 --- a/contrib/draco/cmake/FindDraco.cmake +++ /dev/null @@ -1,56 +0,0 @@ -# Finddraco -# -# Locates draco and sets the following variables: -# -# draco_FOUND draco_INCLUDE_DIRS draco_LIBARY_DIRS draco_LIBRARIES -# draco_VERSION_STRING -# -# draco_FOUND is set to YES only when all other variables are successfully -# configured. - -unset(draco_FOUND) -unset(draco_INCLUDE_DIRS) -unset(draco_LIBRARY_DIRS) -unset(draco_LIBRARIES) -unset(draco_VERSION_STRING) - -mark_as_advanced(draco_FOUND) -mark_as_advanced(draco_INCLUDE_DIRS) -mark_as_advanced(draco_LIBRARY_DIRS) -mark_as_advanced(draco_LIBRARIES) -mark_as_advanced(draco_VERSION_STRING) - -set(draco_version_file_no_prefix "draco/src/draco/core/draco_version.h") - -# Set draco_INCLUDE_DIRS -find_path(draco_INCLUDE_DIRS NAMES "${draco_version_file_no_prefix}") - -# Extract the version string from draco_version.h. -if(draco_INCLUDE_DIRS) - set(draco_version_file - "${draco_INCLUDE_DIRS}/draco/src/draco/core/draco_version.h") - file(STRINGS "${draco_version_file}" draco_version REGEX "kdracoVersion") - list(GET draco_version 0 draco_version) - string(REPLACE "static const char kdracoVersion[] = " "" draco_version - "${draco_version}") - string(REPLACE ";" "" draco_version "${draco_version}") - string(REPLACE "\"" "" draco_version "${draco_version}") - set(draco_VERSION_STRING ${draco_version}) -endif() - -# Find the library. -if(BUILD_SHARED_LIBS) - find_library(draco_LIBRARIES NAMES draco.dll libdraco.dylib libdraco.so) -else() - find_library(draco_LIBRARIES NAMES draco.lib libdraco.a) -endif() - -# Store path to library. -get_filename_component(draco_LIBRARY_DIRS ${draco_LIBRARIES} DIRECTORY) - -if(draco_INCLUDE_DIRS - AND draco_LIBRARY_DIRS - AND draco_LIBRARIES - AND draco_VERSION_STRING) - set(draco_FOUND YES) -endif() diff --git a/contrib/draco/cmake/compiler_flags.cmake b/contrib/draco/cmake/compiler_flags.cmake deleted file mode 100644 index 8750e6f7d..000000000 --- a/contrib/draco/cmake/compiler_flags.cmake +++ /dev/null @@ -1,220 +0,0 @@ -if(DRACO_CMAKE_COMPILER_FLAGS_CMAKE_) - return() -endif() -set(DRACO_CMAKE_COMPILER_FLAGS_CMAKE_ 1) - -include(CheckCCompilerFlag) -include(CheckCXXCompilerFlag) -include("${draco_root}/cmake/compiler_tests.cmake") - -# Strings used to cache failed C/CXX flags. -set(DRACO_FAILED_C_FLAGS) -set(DRACO_FAILED_CXX_FLAGS) - -# Checks C compiler for support of $c_flag. Adds $c_flag to $CMAKE_C_FLAGS when -# the compile test passes. Caches $c_flag in $DRACO_FAILED_C_FLAGS when the test -# fails. -macro(add_c_flag_if_supported c_flag) - unset(C_FLAG_FOUND CACHE) - string(FIND "${CMAKE_C_FLAGS}" "${c_flag}" C_FLAG_FOUND) - unset(C_FLAG_FAILED CACHE) - string(FIND "${DRACO_FAILED_C_FLAGS}" "${c_flag}" C_FLAG_FAILED) - - if(${C_FLAG_FOUND} EQUAL -1 AND ${C_FLAG_FAILED} EQUAL -1) - unset(C_FLAG_SUPPORTED CACHE) - message("Checking C compiler flag support for: " ${c_flag}) - check_c_compiler_flag("${c_flag}" C_FLAG_SUPPORTED) - if(${C_FLAG_SUPPORTED}) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${c_flag}" CACHE STRING "") - else() - set(DRACO_FAILED_C_FLAGS - "${DRACO_FAILED_C_FLAGS} ${c_flag}" - CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Checks C++ compiler for support of $cxx_flag. Adds $cxx_flag to -# $CMAKE_CXX_FLAGS when the compile test passes. Caches $c_flag in -# $DRACO_FAILED_CXX_FLAGS when the test fails. -macro(add_cxx_flag_if_supported cxx_flag) - unset(CXX_FLAG_FOUND CACHE) - string(FIND "${CMAKE_CXX_FLAGS}" "${cxx_flag}" CXX_FLAG_FOUND) - unset(CXX_FLAG_FAILED CACHE) - string(FIND "${DRACO_FAILED_CXX_FLAGS}" "${cxx_flag}" CXX_FLAG_FAILED) - - if(${CXX_FLAG_FOUND} EQUAL -1 AND ${CXX_FLAG_FAILED} EQUAL -1) - unset(CXX_FLAG_SUPPORTED CACHE) - message("Checking CXX compiler flag support for: " ${cxx_flag}) - check_cxx_compiler_flag("${cxx_flag}" CXX_FLAG_SUPPORTED) - if(${CXX_FLAG_SUPPORTED}) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${cxx_flag}" CACHE STRING "") - else() - set(DRACO_FAILED_CXX_FLAGS - "${DRACO_FAILED_CXX_FLAGS} ${cxx_flag}" - CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Convenience method for adding a flag to both the C and C++ compiler command -# lines. -macro(add_compiler_flag_if_supported flag) - add_c_flag_if_supported(${flag}) - add_cxx_flag_if_supported(${flag}) -endmacro() - -# Checks C compiler for support of $c_flag and terminates generation when -# support is not present. -macro(require_c_flag c_flag update_c_flags) - unset(C_FLAG_FOUND CACHE) - string(FIND "${CMAKE_C_FLAGS}" "${c_flag}" C_FLAG_FOUND) - - if(${C_FLAG_FOUND} EQUAL -1) - unset(HAVE_C_FLAG CACHE) - message("Checking C compiler flag support for: " ${c_flag}) - check_c_compiler_flag("${c_flag}" HAVE_C_FLAG) - if(NOT ${HAVE_C_FLAG}) - message( - FATAL_ERROR "${PROJECT_NAME} requires support for C flag: ${c_flag}.") - endif() - if(${update_c_flags}) - set(CMAKE_C_FLAGS "${c_flag} ${CMAKE_C_FLAGS}" CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Checks CXX compiler for support of $cxx_flag and terminates generation when -# support is not present. -macro(require_cxx_flag cxx_flag update_cxx_flags) - unset(CXX_FLAG_FOUND CACHE) - string(FIND "${CMAKE_CXX_FLAGS}" "${cxx_flag}" CXX_FLAG_FOUND) - - if(${CXX_FLAG_FOUND} EQUAL -1) - unset(HAVE_CXX_FLAG CACHE) - message("Checking CXX compiler flag support for: " ${cxx_flag}) - check_cxx_compiler_flag("${cxx_flag}" HAVE_CXX_FLAG) - if(NOT ${HAVE_CXX_FLAG}) - message( - FATAL_ERROR - "${PROJECT_NAME} requires support for CXX flag: ${cxx_flag}.") - endif() - if(${update_cxx_flags}) - set(CMAKE_CXX_FLAGS - "${cxx_flag} ${CMAKE_CXX_FLAGS}" - CACHE STRING "" FORCE) - endif() - endif() -endmacro() - -# Checks for support of $flag by both the C and CXX compilers. Terminates -# generation when support is not present in both compilers. -macro(require_compiler_flag flag update_cmake_flags) - require_c_flag(${flag} ${update_cmake_flags}) - require_cxx_flag(${flag} ${update_cmake_flags}) -endmacro() - -# Checks only non-MSVC targets for support of $c_flag and terminates generation -# when support is not present. -macro(require_c_flag_nomsvc c_flag update_c_flags) - if(NOT MSVC) - require_c_flag(${c_flag} ${update_c_flags}) - endif() -endmacro() - -# Checks only non-MSVC targets for support of $cxx_flag and terminates -# generation when support is not present. -macro(require_cxx_flag_nomsvc cxx_flag update_cxx_flags) - if(NOT MSVC) - require_cxx_flag(${cxx_flag} ${update_cxx_flags}) - endif() -endmacro() - -# Checks only non-MSVC targets for support of $flag by both the C and CXX -# compilers. Terminates generation when support is not present in both -# compilers. -macro(require_compiler_flag_nomsvc flag update_cmake_flags) - require_c_flag_nomsvc(${flag} ${update_cmake_flags}) - require_cxx_flag_nomsvc(${flag} ${update_cmake_flags}) -endmacro() - -# Adds $flag to assembler command line. -macro(append_as_flag flag) - unset(AS_FLAG_FOUND CACHE) - string(FIND "${DRACO_AS_FLAGS}" "${flag}" AS_FLAG_FOUND) - - if(${AS_FLAG_FOUND} EQUAL -1) - set(DRACO_AS_FLAGS "${DRACO_AS_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the C compiler command line. -macro(append_c_flag flag) - unset(C_FLAG_FOUND CACHE) - string(FIND "${CMAKE_C_FLAGS}" "${flag}" C_FLAG_FOUND) - - if(${C_FLAG_FOUND} EQUAL -1) - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the CXX compiler command line. -macro(append_cxx_flag flag) - unset(CXX_FLAG_FOUND CACHE) - string(FIND "${CMAKE_CXX_FLAGS}" "${flag}" CXX_FLAG_FOUND) - - if(${CXX_FLAG_FOUND} EQUAL -1) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the C and CXX compiler command lines. -macro(append_compiler_flag flag) - append_c_flag(${flag}) - append_cxx_flag(${flag}) -endmacro() - -# Adds $flag to the executable linker command line. -macro(append_exe_linker_flag flag) - unset(LINKER_FLAG_FOUND CACHE) - string(FIND "${CMAKE_EXE_LINKER_FLAGS}" "${flag}" LINKER_FLAG_FOUND) - - if(${LINKER_FLAG_FOUND} EQUAL -1) - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${flag}") - endif() -endmacro() - -# Adds $flag to the link flags for $target. -function(append_link_flag_to_target target flags) - unset(target_link_flags) - get_target_property(target_link_flags ${target} LINK_FLAGS) - - if(target_link_flags) - unset(link_flag_found) - string(FIND "${target_link_flags}" "${flags}" link_flag_found) - - if(NOT ${link_flag_found} EQUAL -1) - return() - endif() - - set(target_link_flags "${target_link_flags} ${flags}") - else() - set(target_link_flags "${flags}") - endif() - - set_target_properties(${target} PROPERTIES LINK_FLAGS ${target_link_flags}) -endfunction() - -# Adds $flag to executable linker flags, and makes sure C/CXX builds still work. -macro(require_linker_flag flag) - append_exe_linker_flag(${flag}) - - unset(c_passed) - draco_check_c_compiles("LINKER_FLAG_C_TEST(${flag})" "" c_passed) - unset(cxx_passed) - draco_check_cxx_compiles("LINKER_FLAG_CXX_TEST(${flag})" "" cxx_passed) - - if(NOT c_passed OR NOT cxx_passed) - message(FATAL_ERROR "Linker flag test for ${flag} failed.") - endif() -endmacro() diff --git a/contrib/draco/cmake/compiler_tests.cmake b/contrib/draco/cmake/compiler_tests.cmake deleted file mode 100644 index e781a6537..000000000 --- a/contrib/draco/cmake/compiler_tests.cmake +++ /dev/null @@ -1,103 +0,0 @@ -if(DRACO_CMAKE_COMPILER_TESTS_CMAKE_) - return() -endif() -set(DRACO_CMAKE_COMPILER_TESTS_CMAKE_ 1) - -include(CheckCSourceCompiles) -include(CheckCXXSourceCompiles) - -# The basic main() macro used in all compile tests. -set(DRACO_C_MAIN "\nint main(void) { return 0; }") -set(DRACO_CXX_MAIN "\nint main() { return 0; }") - -# Strings containing the names of passed and failed tests. -set(DRACO_C_PASSED_TESTS) -set(DRACO_C_FAILED_TESTS) -set(DRACO_CXX_PASSED_TESTS) -set(DRACO_CXX_FAILED_TESTS) - -macro(draco_push_var var new_value) - set(SAVED_${var} ${var}) - set(${var} ${new_value}) -endmacro() - -macro(draco_pop_var var) - set(var ${SAVED_${var}}) - unset(SAVED_${var}) -endmacro() - -# Confirms $test_source compiles and stores $test_name in one of -# $DRACO_C_PASSED_TESTS or $DRACO_C_FAILED_TESTS depending on out come. When the -# test passes $result_var is set to 1. When it fails $result_var is unset. The -# test is not run if the test name is found in either of the passed or failed -# test variables. -macro(draco_check_c_compiles test_name test_source result_var) - unset(C_TEST_PASSED CACHE) - unset(C_TEST_FAILED CACHE) - string(FIND "${DRACO_C_PASSED_TESTS}" "${test_name}" C_TEST_PASSED) - string(FIND "${DRACO_C_FAILED_TESTS}" "${test_name}" C_TEST_FAILED) - if(${C_TEST_PASSED} EQUAL -1 AND ${C_TEST_FAILED} EQUAL -1) - unset(C_TEST_COMPILED CACHE) - message("Running C compiler test: ${test_name}") - check_c_source_compiles("${test_source} ${DRACO_C_MAIN}" C_TEST_COMPILED) - set(${result_var} ${C_TEST_COMPILED}) - - if(${C_TEST_COMPILED}) - set(DRACO_C_PASSED_TESTS "${DRACO_C_PASSED_TESTS} ${test_name}") - else() - set(DRACO_C_FAILED_TESTS "${DRACO_C_FAILED_TESTS} ${test_name}") - message("C Compiler test ${test_name} failed.") - endif() - elseif(NOT ${C_TEST_PASSED} EQUAL -1) - set(${result_var} 1) - else() # ${C_TEST_FAILED} NOT EQUAL -1 - unset(${result_var}) - endif() -endmacro() - -# Confirms $test_source compiles and stores $test_name in one of -# $DRACO_CXX_PASSED_TESTS or $DRACO_CXX_FAILED_TESTS depending on out come. When -# the test passes $result_var is set to 1. When it fails $result_var is unset. -# The test is not run if the test name is found in either of the passed or -# failed test variables. -macro(draco_check_cxx_compiles test_name test_source result_var) - unset(CXX_TEST_PASSED CACHE) - unset(CXX_TEST_FAILED CACHE) - string(FIND "${DRACO_CXX_PASSED_TESTS}" "${test_name}" CXX_TEST_PASSED) - string(FIND "${DRACO_CXX_FAILED_TESTS}" "${test_name}" CXX_TEST_FAILED) - if(${CXX_TEST_PASSED} EQUAL -1 AND ${CXX_TEST_FAILED} EQUAL -1) - unset(CXX_TEST_COMPILED CACHE) - message("Running CXX compiler test: ${test_name}") - check_cxx_source_compiles("${test_source} ${DRACO_CXX_MAIN}" - CXX_TEST_COMPILED) - set(${result_var} ${CXX_TEST_COMPILED}) - - if(${CXX_TEST_COMPILED}) - set(DRACO_CXX_PASSED_TESTS "${DRACO_CXX_PASSED_TESTS} ${test_name}") - else() - set(DRACO_CXX_FAILED_TESTS "${DRACO_CXX_FAILED_TESTS} ${test_name}") - message("CXX Compiler test ${test_name} failed.") - endif() - elseif(NOT ${CXX_TEST_PASSED} EQUAL -1) - set(${result_var} 1) - else() # ${CXX_TEST_FAILED} NOT EQUAL -1 - unset(${result_var}) - endif() -endmacro() - -# Convenience macro that confirms $test_source compiles as C and C++. -# $result_var is set to 1 when both tests are successful, and 0 when one or both -# tests fail. Note: This macro is intended to be used to write to result -# variables that are expanded via configure_file(). $result_var is set to 1 or 0 -# to allow direct usage of the value in generated source files. -macro(draco_check_source_compiles test_name test_source result_var) - unset(C_PASSED) - unset(CXX_PASSED) - draco_check_c_compiles(${test_name} ${test_source} C_PASSED) - draco_check_cxx_compiles(${test_name} ${test_source} CXX_PASSED) - if(${C_PASSED} AND ${CXX_PASSED}) - set(${result_var} 1) - else() - set(${result_var} 0) - endif() -endmacro() diff --git a/contrib/draco/cmake/draco-config.cmake.template b/contrib/draco/cmake/draco-config.cmake.template index ca4a456bf..ed86823ea 100644 --- a/contrib/draco/cmake/draco-config.cmake.template +++ b/contrib/draco/cmake/draco-config.cmake.template @@ -1,2 +1,3 @@ -set(DRACO_INCLUDE_DIRS "@DRACO_INCLUDE_DIRS@") -set(DRACO_LIBRARIES "draco") +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/draco-targets.cmake") diff --git a/contrib/draco/cmake/draco.pc.template b/contrib/draco/cmake/draco.pc.template index b8ae48212..050219ccb 100644 --- a/contrib/draco/cmake/draco.pc.template +++ b/contrib/draco/cmake/draco.pc.template @@ -1,11 +1,6 @@ -prefix=@prefix@ -exec_prefix=@exec_prefix@ -libdir=@libdir@ -includedir=@includedir@ - Name: @PROJECT_NAME@ Description: Draco geometry de(com)pression library. Version: @DRACO_VERSION@ -Cflags: -I${includedir} -Libs: -L${libdir} -ldraco +Cflags: -I@includes_path@ +Libs: -L@libs_path@ -ldraco Libs.private: @CMAKE_THREAD_LIBS_INIT@ diff --git a/contrib/draco/cmake/draco_build_definitions.cmake b/contrib/draco/cmake/draco_build_definitions.cmake index f7354c15f..4dc232333 100644 --- a/contrib/draco/cmake/draco_build_definitions.cmake +++ b/contrib/draco/cmake/draco_build_definitions.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_BUILD_DEFINITIONS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_BUILD_DEFINITIONS_CMAKE_ @@ -17,10 +31,6 @@ macro(set_draco_target) endif() set(draco_plugin_dependency draco_static) endif() - - if(BUILD_SHARED_LIBS) - set(CMAKE_POSITION_INDEPENDENT_CODE ON) - endif() endmacro() # Configures flags and sets build system globals. @@ -36,23 +46,37 @@ macro(draco_set_build_definitions) endif() draco_load_version_info() - set(DRACO_SOVERSION 1) + + # Library version info. See the libtool docs for updating the values: + # https://www.gnu.org/software/libtool/manual/libtool.html#Updating-version-info + # + # c=, r=, a= + # + # libtool generates a .so file as .so.[c-a].a.r, while -version-info c:r:a is + # passed to libtool. + # + # We set DRACO_SOVERSION = [c-a].a.r + set(LT_CURRENT 8) + set(LT_REVISION 0) + set(LT_AGE 0) + math(EXPR DRACO_SOVERSION_MAJOR "${LT_CURRENT} - ${LT_AGE}") + set(DRACO_SOVERSION "${DRACO_SOVERSION_MAJOR}.${LT_AGE}.${LT_REVISION}") + unset(LT_CURRENT) + unset(LT_REVISION) + unset(LT_AGE) list(APPEND draco_include_paths "${draco_root}" "${draco_root}/src" "${draco_build}") - if(DRACO_ABSL) - list(APPEND draco_include_path "${draco_root}/third_party/abseil-cpp") + if(DRACO_TRANSCODER_SUPPORTED) + draco_setup_eigen() + draco_setup_filesystem() + draco_setup_tinygltf() + + endif() - list(APPEND draco_gtest_include_paths - "${draco_root}/../googletest/googlemock/include" - "${draco_root}/../googletest/googlemock" - "${draco_root}/../googletest/googletest/include" - "${draco_root}/../googletest/googletest") - list(APPEND draco_test_include_paths ${draco_include_paths} - ${draco_gtest_include_paths}) list(APPEND draco_defines "DRACO_CMAKE=1" "DRACO_FLAGS_SRCDIR=\"${draco_root}\"" "DRACO_FLAGS_TMPDIR=\"/tmp\"") @@ -63,11 +87,22 @@ macro(draco_set_build_definitions) if(BUILD_SHARED_LIBS) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) endif() - else() + endif() + + if(NOT MSVC) if(${CMAKE_SIZEOF_VOID_P} EQUAL 8) # Ensure 64-bit platforms can support large files. list(APPEND draco_defines "_LARGEFILE_SOURCE" "_FILE_OFFSET_BITS=64") endif() + + if(NOT DRACO_DEBUG_COMPILER_WARNINGS) + if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + list(APPEND draco_clang_cxx_flags + "-Wno-implicit-const-int-float-conversion") + else() + list(APPEND draco_base_cxx_flags "-Wno-deprecated-declarations") + endif() + endif() endif() if(ANDROID) @@ -102,13 +137,9 @@ macro(draco_set_build_definitions) set(draco_neon_source_file_suffix "neon.cc") set(draco_sse4_source_file_suffix "sse4.cc") - if((${CMAKE_CXX_COMPILER_ID} - STREQUAL - "GNU" - AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 5) - OR (${CMAKE_CXX_COMPILER_ID} - STREQUAL - "Clang" + if((${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" AND ${CMAKE_CXX_COMPILER_VERSION} + VERSION_LESS 5) + OR (${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang" AND ${CMAKE_CXX_COMPILER_VERSION} VERSION_LESS 4)) message( WARNING "GNU/GCC < v5 or Clang/LLVM < v4, ENABLING COMPATIBILITY MODE.") @@ -117,7 +148,9 @@ macro(draco_set_build_definitions) if(EMSCRIPTEN) draco_check_emscripten_environment() - draco_get_required_emscripten_flags(FLAG_LIST_VAR draco_base_cxx_flags) + draco_get_required_emscripten_flags( + FLAG_LIST_VAR_COMPILER draco_base_cxx_flags + FLAG_LIST_VAR_LINKER draco_base_exe_linker_flags) endif() draco_configure_sanitizer() diff --git a/contrib/draco/cmake/draco_cpu_detection.cmake b/contrib/draco/cmake/draco_cpu_detection.cmake index 96e4a289b..c3b77b80c 100644 --- a/contrib/draco/cmake/draco_cpu_detection.cmake +++ b/contrib/draco/cmake/draco_cpu_detection.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_CPU_DETECTION_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_CPU_DETECTION_CMAKE_ diff --git a/contrib/draco/cmake/draco_dependencies.cmake b/contrib/draco/cmake/draco_dependencies.cmake new file mode 100644 index 000000000..91ee0839b --- /dev/null +++ b/contrib/draco/cmake/draco_dependencies.cmake @@ -0,0 +1,136 @@ +# Copyright 2022 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +if(DRACO_CMAKE_DRACO_DEPENDENCIES_CMAKE) + return() +endif() +set(DRACO_CMAKE_DRACO_DEPENDENCIES_CMAKE 1) + +include("${draco_root}/cmake/draco_variables.cmake") + +# Each variable holds a user specified custom path to a local copy of the +# sources that belong to each project that Draco depends on. When paths are +# empty the build will be generated pointing to the Draco git submodules. +# Otherwise the paths specified by the user will be used in the build +# configuration. + +# Path to the Eigen. The path must contain the Eigen directory. +set(DRACO_EIGEN_PATH) +draco_track_configuration_variable(DRACO_EIGEN_PATH) + +# Path to the gulrak/filesystem installation. The path specified must contain +# the ghc subdirectory that houses the filesystem includes. +set(DRACO_FILESYSTEM_PATH) +draco_track_configuration_variable(DRACO_FILESYSTEM_PATH) + +# Path to the googletest installation. The path must be to the root of the +# Googletest project directory. +set(DRACO_GOOGLETEST_PATH) +draco_track_configuration_variable(DRACO_GOOGLETEST_PATH) + +# Path to the syoyo/tinygltf installation. The path must be to the root of the +# project directory. +set(DRACO_TINYGLTF_PATH) +draco_track_configuration_variable(DRACO_TINYGLTF_PATH) + +# Utility macro for killing the build due to a missing submodule directory. +macro(draco_die_missing_submodule dir) + message(FATAL_ERROR "${dir} missing, run git submodule update --init") +endmacro() + +# Determines the Eigen location and updates the build configuration accordingly. +macro(draco_setup_eigen) + if(DRACO_EIGEN_PATH) + set(eigen_path "${DRACO_EIGEN_PATH}") + + if(NOT IS_DIRECTORY "${eigen_path}") + message(FATAL_ERROR "DRACO_EIGEN_PATH does not exist.") + endif() + else() + set(eigen_path "${draco_root}/third_party/eigen") + + if(NOT IS_DIRECTORY "${eigen_path}") + draco_die_missing_submodule("${eigen_path}") + endif() + endif() + + set(eigen_include_path "${eigen_path}/Eigen") + + if(NOT EXISTS "${eigen_path}/Eigen") + message(FATAL_ERROR "The eigen path does not contain an Eigen directory.") + endif() + + list(APPEND draco_include_paths "${eigen_path}") +endmacro() + +# Determines the gulrak/filesystem location and updates the build configuration +# accordingly. +macro(draco_setup_filesystem) + if(DRACO_FILESYSTEM_PATH) + set(fs_path "${DRACO_FILESYSTEM_PATH}") + + if(NOT IS_DIRECTORY "${fs_path}") + message(FATAL_ERROR "DRACO_FILESYSTEM_PATH does not exist.") + endif() + else() + set(fs_path "${draco_root}/third_party/filesystem/include") + + if(NOT IS_DIRECTORY "${fs_path}") + draco_die_missing_submodule("${fs_path}") + endif() + endif() + + list(APPEND draco_include_paths "${fs_path}") +endmacro() + +# Determines the Googletest location and sets up include and source list vars +# for the draco_tests build. +macro(draco_setup_googletest) + if(DRACO_GOOGLETEST_PATH) + set(gtest_path "${DRACO_GOOGLETEST_PATH}") + if(NOT IS_DIRECTORY "${gtest_path}") + message(FATAL_ERROR "DRACO_GOOGLETEST_PATH does not exist.") + endif() + else() + set(gtest_path "${draco_root}/third_party/googletest") + endif() + + list(APPEND draco_test_include_paths ${draco_include_paths} + "${gtest_path}/include" "${gtest_path}/googlemock" + "${gtest_path}/googletest/include" "${gtest_path}/googletest") + + list(APPEND draco_gtest_all "${gtest_path}/googletest/src/gtest-all.cc") + list(APPEND draco_gtest_main "${gtest_path}/googletest/src/gtest_main.cc") +endmacro() + + +# Determines the location of TinyGLTF and updates the build configuration +# accordingly. +macro(draco_setup_tinygltf) + if(DRACO_TINYGLTF_PATH) + set(tinygltf_path "${DRACO_TINYGLTF_PATH}") + + if(NOT IS_DIRECTORY "${tinygltf_path}") + message(FATAL_ERROR "DRACO_TINYGLTF_PATH does not exist.") + endif() + else() + set(tinygltf_path "${draco_root}/third_party/tinygltf") + + if(NOT IS_DIRECTORY "${tinygltf_path}") + draco_die_missing_submodule("${tinygltf_path}") + endif() + endif() + + list(APPEND draco_include_paths "${tinygltf_path}") +endmacro() diff --git a/contrib/draco/cmake/draco_emscripten.cmake b/contrib/draco/cmake/draco_emscripten.cmake index 10c935043..c9616ae86 100644 --- a/contrib/draco/cmake/draco_emscripten.cmake +++ b/contrib/draco/cmake/draco_emscripten.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_EMSCRIPTEN_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_EMSCRIPTEN_CMAKE_ @@ -18,39 +32,64 @@ endmacro() # Obtains the required Emscripten flags for Draco targets. macro(draco_get_required_emscripten_flags) - set(em_FLAG_LIST_VAR) + set(em_FLAG_LIST_VAR_COMPILER) + set(em_FLAG_LIST_VAR_LINKER) set(em_flags) - set(em_single_arg_opts FLAG_LIST_VAR) + set(em_single_arg_opts FLAG_LIST_VAR_COMPILER FLAG_LIST_VAR_LINKER) set(em_multi_arg_opts) cmake_parse_arguments(em "${em_flags}" "${em_single_arg_opts}" "${em_multi_arg_opts}" ${ARGN}) - if(NOT em_FLAG_LIST_VAR) - message(FATAL "draco_get_required_emscripten_flags: FLAG_LIST_VAR required") + if(NOT em_FLAG_LIST_VAR_COMPILER) + message( + FATAL + "draco_get_required_emscripten_flags: FLAG_LIST_VAR_COMPILER required") + endif() + + if(NOT em_FLAG_LIST_VAR_LINKER) + message( + FATAL + "draco_get_required_emscripten_flags: FLAG_LIST_VAR_LINKER required") endif() if(DRACO_JS_GLUE) unset(required_flags) - list(APPEND ${em_FLAG_LIST_VAR} "-sALLOW_MEMORY_GROWTH=1") - list(APPEND ${em_FLAG_LIST_VAR} "-Wno-almost-asm") - list(APPEND ${em_FLAG_LIST_VAR} "--memory-init-file" "0") - list(APPEND ${em_FLAG_LIST_VAR} "-fno-omit-frame-pointer") - list(APPEND ${em_FLAG_LIST_VAR} "-sMODULARIZE=1") - list(APPEND ${em_FLAG_LIST_VAR} "-sNO_FILESYSTEM=1") - list(APPEND ${em_FLAG_LIST_VAR} "-sEXPORTED_RUNTIME_METHODS=[]") - list(APPEND ${em_FLAG_LIST_VAR} "-sPRECISE_F32=1") - list(APPEND ${em_FLAG_LIST_VAR} "-sNODEJS_CATCH_EXIT=0") - list(APPEND ${em_FLAG_LIST_VAR} "-sNODEJS_CATCH_REJECTION=0") + # TODO(tomfinegan): Revisit splitting of compile/link flags for Emscripten, + # and drop -Wno-unused-command-line-argument. Emscripten complains about + # what are supposedly link-only flags sent with compile commands, but then + # proceeds to produce broken code if the warnings are heeded. + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} + "-Wno-unused-command-line-argument") + + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-Wno-almost-asm") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "--memory-init-file" "0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-fno-omit-frame-pointer") + + # According to Emscripten the following flags are linker only, but sending + # these flags (en masse) to only the linker results in a broken Emscripten + # build with an empty DracoDecoderModule. + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sALLOW_MEMORY_GROWTH=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sMODULARIZE=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sFILESYSTEM=0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} + "-sEXPORTED_FUNCTIONS=[\"_free\",\"_malloc\"]") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sPRECISE_F32=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sNODEJS_CATCH_EXIT=0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sNODEJS_CATCH_REJECTION=0") if(DRACO_FAST) - list(APPEND ${em_FLAG_LIST_VAR} "--llvm-lto" "1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "--llvm-lto" "1") endif() + + # The WASM flag is reported as linker only. if(DRACO_WASM) - list(APPEND ${em_FLAG_LIST_VAR} "-sWASM=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sWASM=1") else() - list(APPEND ${em_FLAG_LIST_VAR} "-sWASM=0") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sWASM=0") endif() + + # The LEGACY_VM_SUPPORT flag is reported as linker only. if(DRACO_IE_COMPATIBLE) - list(APPEND ${em_FLAG_LIST_VAR} "-sLEGACY_VM_SUPPORT=1") + list(APPEND ${em_FLAG_LIST_VAR_COMPILER} "-sLEGACY_VM_SUPPORT=1") endif() endif() endmacro() @@ -66,10 +105,11 @@ macro(draco_generate_emscripten_glue) "${glue_multi_arg_opts}" ${ARGN}) if(DRACO_VERBOSE GREATER 1) - message("--------- draco_generate_emscripten_glue -----------\n" - "glue_INPUT_IDL=${glue_INPUT_IDL}\n" - "glue_OUTPUT_PATH=${glue_OUTPUT_PATH}\n" ] - "----------------------------------------------------\n") + message( + "--------- draco_generate_emscripten_glue -----------\n" + "glue_INPUT_IDL=${glue_INPUT_IDL}\n" + "glue_OUTPUT_PATH=${glue_OUTPUT_PATH}\n" + "----------------------------------------------------\n") endif() if(NOT glue_INPUT_IDL OR NOT glue_OUTPUT_PATH) @@ -79,22 +119,22 @@ macro(draco_generate_emscripten_glue) endif() # Generate the glue source. - execute_process(COMMAND ${PYTHON_EXECUTABLE} - $ENV{EMSCRIPTEN}/tools/webidl_binder.py - ${glue_INPUT_IDL} ${glue_OUTPUT_PATH}) + execute_process( + COMMAND ${PYTHON_EXECUTABLE} $ENV{EMSCRIPTEN}/tools/webidl_binder.py + ${glue_INPUT_IDL} ${glue_OUTPUT_PATH}) if(NOT EXISTS "${glue_OUTPUT_PATH}.cpp") message(FATAL_ERROR "JS glue generation failed for ${glue_INPUT_IDL}.") endif() # Create a dependency so that it regenerated on edits. - add_custom_command(OUTPUT "${glue_OUTPUT_PATH}.cpp" - COMMAND ${PYTHON_EXECUTABLE} - $ENV{EMSCRIPTEN}/tools/webidl_binder.py - ${glue_INPUT_IDL} ${glue_OUTPUT_PATH} - DEPENDS ${draco_js_dec_idl} - COMMENT "Generating ${glue_OUTPUT_PATH}.cpp." - WORKING_DIRECTORY ${draco_build} - VERBATIM) + add_custom_command( + OUTPUT "${glue_OUTPUT_PATH}.cpp" + COMMAND ${PYTHON_EXECUTABLE} $ENV{EMSCRIPTEN}/tools/webidl_binder.py + ${glue_INPUT_IDL} ${glue_OUTPUT_PATH} + DEPENDS ${draco_js_dec_idl} + COMMENT "Generating ${glue_OUTPUT_PATH}.cpp." + WORKING_DIRECTORY ${draco_build} + VERBATIM) endmacro() # Wrapper for draco_add_executable() that handles the extra work necessary for @@ -120,8 +160,14 @@ macro(draco_add_emscripten_executable) unset(emexe_LINK_FLAGS) set(optional_args) set(single_value_args NAME GLUE_PATH) - set(multi_value_args SOURCES DEFINES FEATURES INCLUDES LINK_FLAGS - PRE_LINK_JS_SOURCES POST_LINK_JS_SOURCES) + set(multi_value_args + SOURCES + DEFINES + FEATURES + INCLUDES + LINK_FLAGS + PRE_LINK_JS_SOURCES + POST_LINK_JS_SOURCES) cmake_parse_arguments(emexe "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) @@ -136,49 +182,50 @@ macro(draco_add_emscripten_executable) endif() if(DRACO_VERBOSE GREATER 1) - message("--------- draco_add_emscripten_executable ---------\n" - "emexe_NAME=${emexe_NAME}\n" - "emexe_SOURCES=${emexe_SOURCES}\n" - "emexe_DEFINES=${emexe_DEFINES}\n" - "emexe_INCLUDES=${emexe_INCLUDES}\n" - "emexe_LINK_FLAGS=${emexe_LINK_FLAGS}\n" - "emexe_GLUE_PATH=${emexe_GLUE_PATH}\n" - "emexe_FEATURES=${emexe_FEATURES}\n" - "emexe_PRE_LINK_JS_SOURCES=${emexe_PRE_LINK_JS_SOURCES}\n" - "emexe_POST_LINK_JS_SOURCES=${emexe_POST_LINK_JS_SOURCES}\n" - "----------------------------------------------------\n") + message( + "--------- draco_add_emscripten_executable ---------\n" + "emexe_NAME=${emexe_NAME}\n" + "emexe_SOURCES=${emexe_SOURCES}\n" + "emexe_DEFINES=${emexe_DEFINES}\n" + "emexe_INCLUDES=${emexe_INCLUDES}\n" + "emexe_LINK_FLAGS=${emexe_LINK_FLAGS}\n" + "emexe_GLUE_PATH=${emexe_GLUE_PATH}\n" + "emexe_FEATURES=${emexe_FEATURES}\n" + "emexe_PRE_LINK_JS_SOURCES=${emexe_PRE_LINK_JS_SOURCES}\n" + "emexe_POST_LINK_JS_SOURCES=${emexe_POST_LINK_JS_SOURCES}\n" + "----------------------------------------------------\n") endif() # The Emscripten linker needs the C++ flags in addition to whatever has been # passed in with the target. list(APPEND emexe_LINK_FLAGS ${DRACO_CXX_FLAGS}) - if(DRACO_GLTF) - draco_add_executable(NAME - ${emexe_NAME} - OUTPUT_NAME - ${emexe_NAME}_gltf - SOURCES - ${emexe_SOURCES} - DEFINES - ${emexe_DEFINES} - INCLUDES - ${emexe_INCLUDES} - LINK_FLAGS - ${emexe_LINK_FLAGS}) + if(DRACO_GLTF_BITSTREAM) + # Add "_gltf" suffix to target output name. + draco_add_executable( + NAME ${emexe_NAME} + OUTPUT_NAME ${emexe_NAME}_gltf + SOURCES ${emexe_SOURCES} + DEFINES ${emexe_DEFINES} + INCLUDES ${emexe_INCLUDES} + LINK_FLAGS ${emexe_LINK_FLAGS}) else() - draco_add_executable(NAME ${emexe_NAME} SOURCES ${emexe_SOURCES} DEFINES - ${emexe_DEFINES} INCLUDES ${emexe_INCLUDES} LINK_FLAGS - ${emexe_LINK_FLAGS}) + draco_add_executable( + NAME ${emexe_NAME} + SOURCES ${emexe_SOURCES} + DEFINES ${emexe_DEFINES} + INCLUDES ${emexe_INCLUDES} + LINK_FLAGS ${emexe_LINK_FLAGS}) endif() foreach(feature ${emexe_FEATURES}) draco_enable_feature(FEATURE ${feature} TARGETS ${emexe_NAME}) endforeach() - set_property(SOURCE ${emexe_SOURCES} - APPEND - PROPERTY OBJECT_DEPENDS "${emexe_GLUE_PATH}.cpp") + set_property( + SOURCE ${emexe_SOURCES} + APPEND + PROPERTY OBJECT_DEPENDS "${emexe_GLUE_PATH}.cpp") em_link_pre_js(${emexe_NAME} ${emexe_PRE_LINK_JS_SOURCES}) em_link_post_js(${emexe_NAME} "${emexe_GLUE_PATH}.js" ${emexe_POST_LINK_JS_SOURCES}) diff --git a/contrib/draco/cmake/draco_flags.cmake b/contrib/draco/cmake/draco_flags.cmake index 0397859a4..f3b24c6e1 100644 --- a/contrib/draco/cmake/draco_flags.cmake +++ b/contrib/draco/cmake/draco_flags.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_FLAGS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_FLAGS_CMAKE_ @@ -24,7 +38,7 @@ macro(draco_set_compiler_flags_for_sources) endif() set_source_files_properties(${compiler_SOURCES} PROPERTIES COMPILE_FLAGS - ${compiler_FLAGS}) + ${compiler_FLAGS}) if(DRACO_VERBOSE GREATER 1) foreach(source ${compiler_SOURCES}) @@ -85,8 +99,8 @@ macro(draco_test_cxx_flag) # are passed as a list it will remove the list separators, and attempt to run # a compile command using list entries concatenated together as a single # argument. Avoid the problem by forcing the argument to be a string. - draco_set_and_stringify(SOURCE_VARS all_cxx_flags DEST all_cxx_flags) - check_cxx_compiler_flag("${all_cxx_flags}" draco_all_cxx_flags_pass) + draco_set_and_stringify(SOURCE_VARS all_cxx_flags DEST all_cxx_flags_string) + check_cxx_compiler_flag("${all_cxx_flags_string}" draco_all_cxx_flags_pass) if(cxx_test_FLAG_REQUIRED AND NOT draco_all_cxx_flags_pass) draco_die("Flag test failed for required flag(s): " @@ -245,3 +259,34 @@ macro(draco_set_cxx_flags) draco_test_cxx_flag(FLAG_LIST_VAR_NAMES ${cxx_flag_lists}) endif() endmacro() + +# Collects Draco built-in and user-specified linker flags and tests them. Halts +# configuration and reports the error when any flags cause the build to fail. +# +# Note: draco_test_exe_linker_flag() does the real work of setting the flags and +# running the test compile commands. +macro(draco_set_exe_linker_flags) + unset(linker_flag_lists) + + if(DRACO_VERBOSE) + message("draco_set_exe_linker_flags: " + "draco_base_exe_linker_flags=${draco_base_exe_linker_flags}") + endif() + + if(draco_base_exe_linker_flags) + list(APPEND linker_flag_lists draco_base_exe_linker_flags) + endif() + + if(linker_flag_lists) + unset(test_linker_flags) + + if(DRACO_VERBOSE) + message("draco_set_exe_linker_flags: " + "linker_flag_lists=${linker_flag_lists}") + endif() + + draco_set_and_stringify(DEST test_linker_flags SOURCE_VARS + ${linker_flag_lists}) + draco_test_exe_linker_flag(FLAG_LIST_VAR_NAME test_linker_flags) + endif() +endmacro() diff --git a/contrib/draco/cmake/draco_helpers.cmake b/contrib/draco/cmake/draco_helpers.cmake index 0b3b804cf..69e24c5be 100644 --- a/contrib/draco/cmake/draco_helpers.cmake +++ b/contrib/draco/cmake/draco_helpers.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_HELPERS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_HELPERS_CMAKE_ diff --git a/contrib/draco/cmake/draco_install.cmake b/contrib/draco/cmake/draco_install.cmake index 09bfb591d..3be1ba163 100644 --- a/contrib/draco/cmake/draco_install.cmake +++ b/contrib/draco/cmake/draco_install.cmake @@ -1,32 +1,32 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_INSTALL_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_INSTALL_CMAKE_ set(DRACO_CMAKE_DRACO_INSTALL_CMAKE_ 1) +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + # Sets up the draco install targets. Must be called after the static library # target is created. macro(draco_setup_install_target) - include(GNUInstallDirs) - - # pkg-config: draco.pc - set(prefix "${CMAKE_INSTALL_PREFIX}") - set(exec_prefix "\${prefix}") - set(libdir "\${prefix}/${CMAKE_INSTALL_LIBDIR}") - set(includedir "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") - set(draco_lib_name "draco") - - configure_file("${draco_root}/cmake/draco.pc.template" - "${draco_build}/draco.pc" @ONLY NEWLINE_STYLE UNIX) - install(FILES "${draco_build}/draco.pc" - DESTINATION "${prefix}/${CMAKE_INSTALL_LIBDIR}/pkgconfig") - - # CMake config: draco-config.cmake - set(DRACO_INCLUDE_DIRS "${prefix}/${CMAKE_INSTALL_INCLUDEDIR}") - configure_file("${draco_root}/cmake/draco-config.cmake.template" - "${draco_build}/draco-config.cmake" @ONLY NEWLINE_STYLE UNIX) - install( - FILES "${draco_build}/draco-config.cmake" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATAROOTDIR}/cmake") + set(bin_path "${CMAKE_INSTALL_BINDIR}") + set(data_path "${CMAKE_INSTALL_DATAROOTDIR}") + set(includes_path "${CMAKE_INSTALL_INCLUDEDIR}") + set(libs_path "${CMAKE_INSTALL_LIBDIR}") foreach(file ${draco_sources}) if(file MATCHES "h$") @@ -34,46 +34,88 @@ macro(draco_setup_install_target) endif() endforeach() + list(REMOVE_DUPLICATES draco_api_includes) + # Strip $draco_src_root from the file paths: we need to install relative to # $include_directory. list(TRANSFORM draco_api_includes REPLACE "${draco_src_root}/" "") - set(include_directory "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}") foreach(draco_api_include ${draco_api_includes}) get_filename_component(file_directory ${draco_api_include} DIRECTORY) - set(target_directory "${include_directory}/draco/${file_directory}") + set(target_directory "${includes_path}/draco/${file_directory}") install(FILES ${draco_src_root}/${draco_api_include} DESTINATION "${target_directory}") endforeach() - install( - FILES "${draco_build}/draco/draco_features.h" - DESTINATION "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_INCLUDEDIR}/draco/") + install(FILES "${draco_build}/draco/draco_features.h" + DESTINATION "${includes_path}/draco/") - install(TARGETS draco_decoder DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") - install(TARGETS draco_encoder DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}") + install(TARGETS draco_decoder DESTINATION "${bin_path}") + install(TARGETS draco_encoder DESTINATION "${bin_path}") + + if(DRACO_TRANSCODER_SUPPORTED) + install(TARGETS draco_transcoder DESTINATION "${bin_path}") + endif() if(MSVC) - install(TARGETS draco DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install( + TARGETS draco + EXPORT dracoExport + RUNTIME DESTINATION "${bin_path}" + ARCHIVE DESTINATION "${libs_path}" + LIBRARY DESTINATION "${libs_path}") else() - install(TARGETS draco_static DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install( + TARGETS draco_static + EXPORT dracoExport + DESTINATION "${libs_path}") + if(BUILD_SHARED_LIBS) - install(TARGETS draco_shared DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install( + TARGETS draco_shared + EXPORT dracoExport + RUNTIME DESTINATION "${bin_path}" + ARCHIVE DESTINATION "${libs_path}" + LIBRARY DESTINATION "${libs_path}") endif() endif() if(DRACO_UNITY_PLUGIN) - install(TARGETS dracodec_unity DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") - endif() - if(DRACO_MAYA_PLUGIN) - install(TARGETS draco_maya_wrapper DESTINATION - "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") + install(TARGETS dracodec_unity DESTINATION "${libs_path}") endif() + if(DRACO_MAYA_PLUGIN) + install(TARGETS draco_maya_wrapper DESTINATION "${libs_path}") + endif() + + # pkg-config: draco.pc + configure_file("${draco_root}/cmake/draco.pc.template" + "${draco_build}/draco.pc" @ONLY NEWLINE_STYLE UNIX) + install(FILES "${draco_build}/draco.pc" DESTINATION "${libs_path}/pkgconfig") + + # CMake config: draco-config.cmake + configure_package_config_file( + "${draco_root}/cmake/draco-config.cmake.template" + "${draco_build}/draco-config.cmake" + INSTALL_DESTINATION "${data_path}/cmake/draco") + + write_basic_package_version_file( + "${draco_build}/draco-config-version.cmake" + VERSION ${DRACO_VERSION} + COMPATIBILITY AnyNewerVersion) + + export( + EXPORT dracoExport + NAMESPACE draco:: + FILE "${draco_build}/draco-targets.cmake") + + install( + EXPORT dracoExport + NAMESPACE draco:: + FILE draco-targets.cmake + DESTINATION "${data_path}/cmake/draco") + + install(FILES "${draco_build}/draco-config.cmake" + "${draco_build}/draco-config-version.cmake" + DESTINATION "${data_path}/cmake/draco") endmacro() diff --git a/contrib/draco/cmake/draco_intrinsics.cmake b/contrib/draco/cmake/draco_intrinsics.cmake index 9011c0de5..178df97a6 100644 --- a/contrib/draco/cmake/draco_intrinsics.cmake +++ b/contrib/draco/cmake/draco_intrinsics.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_INTRINSICS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_INTRINSICS_CMAKE_ @@ -61,17 +75,15 @@ macro(draco_process_intrinsics_sources) unset(sse4_sources) list(APPEND sse4_sources ${arg_SOURCES}) - list(FILTER sse4_sources INCLUDE REGEX - "${draco_sse4_source_file_suffix}$") + list(FILTER sse4_sources INCLUDE REGEX "${draco_sse4_source_file_suffix}$") if(sse4_sources) unset(sse4_flags) - draco_get_intrinsics_flag_for_suffix(SUFFIX - ${draco_sse4_source_file_suffix} - VARIABLE sse4_flags) + draco_get_intrinsics_flag_for_suffix( + SUFFIX ${draco_sse4_source_file_suffix} VARIABLE sse4_flags) if(sse4_flags) draco_set_compiler_flags_for_sources(SOURCES ${sse4_sources} FLAGS - ${sse4_flags}) + ${sse4_flags}) endif() endif() endif() @@ -79,17 +91,15 @@ macro(draco_process_intrinsics_sources) if(DRACO_ENABLE_NEON AND draco_have_neon) unset(neon_sources) list(APPEND neon_sources ${arg_SOURCES}) - list(FILTER neon_sources INCLUDE REGEX - "${draco_neon_source_file_suffix}$") + list(FILTER neon_sources INCLUDE REGEX "${draco_neon_source_file_suffix}$") if(neon_sources AND DRACO_NEON_INTRINSICS_FLAG) unset(neon_flags) - draco_get_intrinsics_flag_for_suffix(SUFFIX - ${draco_neon_source_file_suffix} - VARIABLE neon_flags) + draco_get_intrinsics_flag_for_suffix( + SUFFIX ${draco_neon_source_file_suffix} VARIABLE neon_flags) if(neon_flags) draco_set_compiler_flags_for_sources(SOURCES ${neon_sources} FLAGS - ${neon_flags}) + ${neon_flags}) endif() endif() endif() diff --git a/contrib/draco/cmake/draco_options.cmake b/contrib/draco/cmake/draco_options.cmake index 832bfb69f..085149774 100644 --- a/contrib/draco/cmake/draco_options.cmake +++ b/contrib/draco/cmake/draco_options.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_OPTIONS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_OPTIONS_CMAKE_ @@ -18,17 +32,22 @@ macro(draco_option) cmake_parse_arguments(option "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) - if(NOT (option_NAME AND option_HELPSTRING AND DEFINED option_VALUE)) + if(NOT + (option_NAME + AND option_HELPSTRING + AND DEFINED option_VALUE)) message(FATAL_ERROR "draco_option: NAME HELPSTRING and VALUE required.") endif() option(${option_NAME} ${option_HELPSTRING} ${option_VALUE}) if(DRACO_VERBOSE GREATER 2) - message("--------- draco_option ---------\n" "option_NAME=${option_NAME}\n" - "option_HELPSTRING=${option_HELPSTRING}\n" - "option_VALUE=${option_VALUE}\n" - "------------------------------------------\n") + message( + "--------- draco_option ---------\n" + "option_NAME=${option_NAME}\n" + "option_HELPSTRING=${option_HELPSTRING}\n" + "option_VALUE=${option_VALUE}\n" + "------------------------------------------\n") endif() list(APPEND draco_options ${option_NAME}) @@ -44,33 +63,74 @@ endmacro() # Set default options. macro(draco_set_default_options) - draco_option(NAME DRACO_FAST HELPSTRING "Try to build faster libs." VALUE OFF) - draco_option(NAME DRACO_JS_GLUE HELPSTRING - "Enable JS Glue and JS targets when using Emscripten." VALUE ON) - draco_option(NAME DRACO_IE_COMPATIBLE HELPSTRING - "Enable support for older IE builds when using Emscripten." VALUE - OFF) - draco_option(NAME DRACO_MESH_COMPRESSION HELPSTRING "Enable mesh compression." - VALUE ON) - draco_option(NAME DRACO_POINT_CLOUD_COMPRESSION HELPSTRING - "Enable point cloud compression." VALUE ON) - draco_option(NAME DRACO_PREDICTIVE_EDGEBREAKER HELPSTRING - "Enable predictive edgebreaker." VALUE ON) - draco_option(NAME DRACO_STANDARD_EDGEBREAKER HELPSTRING - "Enable stand edgebreaker." VALUE ON) - draco_option(NAME DRACO_BACKWARDS_COMPATIBILITY HELPSTRING - "Enable backwards compatibility." VALUE ON) - draco_option(NAME DRACO_DECODER_ATTRIBUTE_DEDUPLICATION HELPSTRING - "Enable attribute deduping." VALUE OFF) - draco_option(NAME DRACO_TESTS HELPSTRING "Enables tests." VALUE OFF) - draco_option(NAME DRACO_WASM HELPSTRING "Enables WASM support." VALUE OFF) - draco_option(NAME DRACO_UNITY_PLUGIN HELPSTRING - "Build plugin library for Unity." VALUE OFF) - draco_option(NAME DRACO_ANIMATION_ENCODING HELPSTRING "Enable animation." - VALUE OFF) - draco_option(NAME DRACO_GLTF HELPSTRING "Support GLTF." VALUE OFF) - draco_option(NAME DRACO_MAYA_PLUGIN HELPSTRING - "Build plugin library for Maya." VALUE OFF) + draco_option( + NAME DRACO_FAST + HELPSTRING "Try to build faster libs." + VALUE OFF) + draco_option( + NAME DRACO_JS_GLUE + HELPSTRING "Enable JS Glue and JS targets when using Emscripten." + VALUE ON) + draco_option( + NAME DRACO_IE_COMPATIBLE + HELPSTRING "Enable support for older IE builds when using Emscripten." + VALUE OFF) + draco_option( + NAME DRACO_MESH_COMPRESSION + HELPSTRING "Enable mesh compression." + VALUE ON) + draco_option( + NAME DRACO_POINT_CLOUD_COMPRESSION + HELPSTRING "Enable point cloud compression." + VALUE ON) + draco_option( + NAME DRACO_PREDICTIVE_EDGEBREAKER + HELPSTRING "Enable predictive edgebreaker." + VALUE ON) + draco_option( + NAME DRACO_STANDARD_EDGEBREAKER + HELPSTRING "Enable stand edgebreaker." + VALUE ON) + draco_option( + NAME DRACO_BACKWARDS_COMPATIBILITY + HELPSTRING "Enable backwards compatibility." + VALUE ON) + draco_option( + NAME DRACO_DECODER_ATTRIBUTE_DEDUPLICATION + HELPSTRING "Enable attribute deduping." + VALUE OFF) + draco_option( + NAME DRACO_TESTS + HELPSTRING "Enables tests." + VALUE OFF) + draco_option( + NAME DRACO_WASM + HELPSTRING "Enables WASM support." + VALUE OFF) + draco_option( + NAME DRACO_UNITY_PLUGIN + HELPSTRING "Build plugin library for Unity." + VALUE OFF) + draco_option( + NAME DRACO_ANIMATION_ENCODING + HELPSTRING "Enable animation." + VALUE OFF) + draco_option( + NAME DRACO_GLTF_BITSTREAM + HELPSTRING "Draco GLTF extension bitstream specified features only." + VALUE OFF) + draco_option( + NAME DRACO_MAYA_PLUGIN + HELPSTRING "Build plugin library for Maya." + VALUE OFF) + draco_option( + NAME DRACO_TRANSCODER_SUPPORTED + HELPSTRING "Enable the Draco transcoder." + VALUE OFF) + draco_option( + NAME DRACO_DEBUG_COMPILER_WARNINGS + HELPSTRING "Turn on more warnings." + VALUE OFF) draco_check_deprecated_options() endmacro() @@ -117,14 +177,16 @@ macro(draco_check_deprecated_options) DRACO_MAYA_PLUGIN) draco_handle_deprecated_option(OLDNAME BUILD_USD_PLUGIN NEWNAME BUILD_SHARED_LIBS) + draco_handle_deprecated_option(OLDNAME DRACO_GLTF NEWNAME + DRACO_GLTF_BITSTREAM) endmacro() # Macro for setting Draco features based on user configuration. Features enabled # by this macro are Draco global. macro(draco_set_optional_features) - if(DRACO_GLTF) - # Override settings when building for GLTF. + if(DRACO_GLTF_BITSTREAM) + # Enable only the features included in the Draco GLTF bitstream spec. draco_enable_feature(FEATURE "DRACO_MESH_COMPRESSION_SUPPORTED") draco_enable_feature(FEATURE "DRACO_NORMAL_ENCODING_SUPPORTED") draco_enable_feature(FEATURE "DRACO_STANDARD_EDGEBREAKER_SUPPORTED") @@ -170,6 +232,11 @@ macro(draco_set_optional_features) set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() + if(DRACO_TRANSCODER_SUPPORTED) + draco_enable_feature(FEATURE "DRACO_TRANSCODER_SUPPORTED") + endif() + + endmacro() # Macro that handles tracking of Draco preprocessor symbols for the purpose of @@ -221,8 +288,56 @@ function(draco_generate_features_h) file(APPEND "${draco_features_file_name}.new" "#define ${feature}\n") endforeach() + if(MSVC) + if(NOT DRACO_DEBUG_COMPILER_WARNINGS) + file(APPEND "${draco_features_file_name}.new" + "// Enable DRACO_DEBUG_COMPILER_WARNINGS at CMake generation \n" + "// time to remove these pragmas.\n") + + # warning C4018: '': signed/unsigned mismatch. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4018)\n") + + # warning C4146: unary minus operator applied to unsigned type, result + # still unsigned + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4146)\n") + + # warning C4244: 'return': conversion from '' to '', possible + # loss of data. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4244)\n") + + # warning C4267: 'initializing' conversion from '' to '', + # possible loss of data. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4267)\n") + + # warning C4305: 'context' : truncation from 'type1' to 'type2'. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4305)\n") + + # warning C4661: 'identifier' : no suitable definition provided for + # explicit template instantiation request. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4661)\n") + + # warning C4800: Implicit conversion from 'type' to bool. Possible + # information loss. + # Also, in older MSVC releases: + # warning C4800: 'type' : forcing value to bool 'true' or 'false' + # (performance warning). + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4800)\n") + + # warning C4804: '': unsafe use of type '' in operation. + file(APPEND "${draco_features_file_name}.new" + "#pragma warning(disable:4804)\n") + endif() + endif() + file(APPEND "${draco_features_file_name}.new" - "\n#endif // DRACO_FEATURES_H_") + "\n#endif // DRACO_FEATURES_H_\n") # Will replace ${draco_features_file_name} only if the file content has # changed. This prevents forced Draco rebuilds after CMake runs. diff --git a/contrib/draco/cmake/draco_sanitizer.cmake b/contrib/draco/cmake/draco_sanitizer.cmake index d2e41a6cb..77d141481 100644 --- a/contrib/draco/cmake/draco_sanitizer.cmake +++ b/contrib/draco/cmake/draco_sanitizer.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_SANITIZER_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_SANITIZER_CMAKE_ @@ -5,7 +19,9 @@ set(DRACO_CMAKE_DRACO_SANITIZER_CMAKE_ 1) # Handles the details of enabling sanitizers. macro(draco_configure_sanitizer) - if(DRACO_SANITIZE AND NOT EMSCRIPTEN AND NOT MSVC) + if(DRACO_SANITIZE + AND NOT EMSCRIPTEN + AND NOT MSVC) if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") if(DRACO_SANITIZE MATCHES "cfi") list(APPEND SAN_CXX_FLAGS "-flto" "-fno-sanitize-trap=cfi") @@ -13,8 +29,8 @@ macro(draco_configure_sanitizer) "-fuse-ld=gold") endif() - if(${CMAKE_SIZEOF_VOID_P} EQUAL 4 - AND DRACO_SANITIZE MATCHES "integer|undefined") + if(${CMAKE_SIZEOF_VOID_P} EQUAL 4 AND DRACO_SANITIZE MATCHES + "integer|undefined") list(APPEND SAN_LINKER_FLAGS "--rtlib=compiler-rt" "-lgcc_s") endif() endif() diff --git a/contrib/draco/cmake/draco_targets.cmake b/contrib/draco/cmake/draco_targets.cmake index 0456c4d7b..c8c79f511 100644 --- a/contrib/draco/cmake/draco_targets.cmake +++ b/contrib/draco/cmake/draco_targets.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_TARGETS_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_TARGETS_CMAKE_ @@ -51,26 +65,33 @@ macro(draco_add_executable) unset(exe_LIB_DEPS) set(optional_args TEST) set(single_value_args NAME OUTPUT_NAME) - set(multi_value_args SOURCES DEFINES INCLUDES COMPILE_FLAGS LINK_FLAGS - OBJLIB_DEPS LIB_DEPS) + set(multi_value_args + SOURCES + DEFINES + INCLUDES + COMPILE_FLAGS + LINK_FLAGS + OBJLIB_DEPS + LIB_DEPS) cmake_parse_arguments(exe "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) if(DRACO_VERBOSE GREATER 1) - message("--------- draco_add_executable ---------\n" - "exe_TEST=${exe_TEST}\n" - "exe_TEST_DEFINES_MAIN=${exe_TEST_DEFINES_MAIN}\n" - "exe_NAME=${exe_NAME}\n" - "exe_OUTPUT_NAME=${exe_OUTPUT_NAME}\n" - "exe_SOURCES=${exe_SOURCES}\n" - "exe_DEFINES=${exe_DEFINES}\n" - "exe_INCLUDES=${exe_INCLUDES}\n" - "exe_COMPILE_FLAGS=${exe_COMPILE_FLAGS}\n" - "exe_LINK_FLAGS=${exe_LINK_FLAGS}\n" - "exe_OBJLIB_DEPS=${exe_OBJLIB_DEPS}\n" - "exe_LIB_DEPS=${exe_LIB_DEPS}\n" - "------------------------------------------\n") + message( + "--------- draco_add_executable ---------\n" + "exe_TEST=${exe_TEST}\n" + "exe_TEST_DEFINES_MAIN=${exe_TEST_DEFINES_MAIN}\n" + "exe_NAME=${exe_NAME}\n" + "exe_OUTPUT_NAME=${exe_OUTPUT_NAME}\n" + "exe_SOURCES=${exe_SOURCES}\n" + "exe_DEFINES=${exe_DEFINES}\n" + "exe_INCLUDES=${exe_INCLUDES}\n" + "exe_COMPILE_FLAGS=${exe_COMPILE_FLAGS}\n" + "exe_LINK_FLAGS=${exe_LINK_FLAGS}\n" + "exe_OBJLIB_DEPS=${exe_OBJLIB_DEPS}\n" + "exe_LIB_DEPS=${exe_LIB_DEPS}\n" + "------------------------------------------\n") endif() if(NOT (exe_NAME AND exe_SOURCES)) @@ -87,7 +108,12 @@ macro(draco_add_executable) endif() add_executable(${exe_NAME} ${exe_SOURCES}) - set_target_properties(${exe_NAME} PROPERTIES VERSION ${DRACO_VERSION}) + + target_compile_features(${exe_NAME} PUBLIC cxx_std_11) + + if(NOT EMSCRIPTEN) + set_target_properties(${exe_NAME} PROPERTIES VERSION ${DRACO_VERSION}) + endif() if(exe_OUTPUT_NAME) set_target_properties(${exe_NAME} PROPERTIES OUTPUT_NAME ${exe_OUTPUT_NAME}) @@ -104,8 +130,8 @@ macro(draco_add_executable) endif() if(exe_COMPILE_FLAGS OR DRACO_CXX_FLAGS) - target_compile_options(${exe_NAME} - PRIVATE ${exe_COMPILE_FLAGS} ${DRACO_CXX_FLAGS}) + target_compile_options(${exe_NAME} PRIVATE ${exe_COMPILE_FLAGS} + ${DRACO_CXX_FLAGS}) endif() if(exe_LINK_FLAGS OR DRACO_EXE_LINKER_FLAGS) @@ -113,8 +139,8 @@ macro(draco_add_executable) list(APPEND exe_LINK_FLAGS "${DRACO_EXE_LINKER_FLAGS}") # LINK_FLAGS is managed as a string. draco_set_and_stringify(SOURCE "${exe_LINK_FLAGS}" DEST exe_LINK_FLAGS) - set_target_properties(${exe_NAME} - PROPERTIES LINK_FLAGS "${exe_LINK_FLAGS}") + set_target_properties(${exe_NAME} PROPERTIES LINK_FLAGS + "${exe_LINK_FLAGS}") else() target_link_options(${exe_NAME} PRIVATE ${exe_LINK_FLAGS} ${DRACO_EXE_LINKER_FLAGS}) @@ -136,12 +162,7 @@ macro(draco_add_executable) endif() if(exe_LIB_DEPS) - unset(exe_static) - if("${CMAKE_EXE_LINKER_FLAGS} ${DRACO_EXE_LINKER_FLAGS}" MATCHES "static") - set(exe_static ON) - endif() - - if(exe_static AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + if(CMAKE_CXX_COMPILER_ID MATCHES "^Clang|^GNU") # Third party dependencies can introduce dependencies on system and test # libraries. Since the target created here is an executable, and CMake # does not provide a method of controlling order of link dependencies, @@ -149,6 +170,10 @@ macro(draco_add_executable) # ensure that dependencies of third party targets can be resolved when # those dependencies happen to be resolved by dependencies of the current # target. + # TODO(tomfinegan): For portability use LINK_GROUP with RESCAN instead of + # directly (ab)using compiler/linker specific flags once CMake v3.24 is in + # wider use. See: + # https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html#genex:LINK_GROUP list(INSERT exe_LIB_DEPS 0 -Wl,--start-group) list(APPEND exe_LIB_DEPS -Wl,--end-group) endif() @@ -209,27 +234,36 @@ macro(draco_add_library) unset(lib_TARGET_PROPERTIES) set(optional_args TEST) set(single_value_args NAME OUTPUT_NAME TYPE) - set(multi_value_args SOURCES DEFINES INCLUDES COMPILE_FLAGS LINK_FLAGS - OBJLIB_DEPS LIB_DEPS PUBLIC_INCLUDES TARGET_PROPERTIES) + set(multi_value_args + SOURCES + DEFINES + INCLUDES + COMPILE_FLAGS + LINK_FLAGS + OBJLIB_DEPS + LIB_DEPS + PUBLIC_INCLUDES + TARGET_PROPERTIES) cmake_parse_arguments(lib "${optional_args}" "${single_value_args}" "${multi_value_args}" ${ARGN}) if(DRACO_VERBOSE GREATER 1) - message("--------- draco_add_library ---------\n" - "lib_TEST=${lib_TEST}\n" - "lib_NAME=${lib_NAME}\n" - "lib_OUTPUT_NAME=${lib_OUTPUT_NAME}\n" - "lib_TYPE=${lib_TYPE}\n" - "lib_SOURCES=${lib_SOURCES}\n" - "lib_DEFINES=${lib_DEFINES}\n" - "lib_INCLUDES=${lib_INCLUDES}\n" - "lib_COMPILE_FLAGS=${lib_COMPILE_FLAGS}\n" - "lib_LINK_FLAGS=${lib_LINK_FLAGS}\n" - "lib_OBJLIB_DEPS=${lib_OBJLIB_DEPS}\n" - "lib_LIB_DEPS=${lib_LIB_DEPS}\n" - "lib_PUBLIC_INCLUDES=${lib_PUBLIC_INCLUDES}\n" - "---------------------------------------\n") + message( + "--------- draco_add_library ---------\n" + "lib_TEST=${lib_TEST}\n" + "lib_NAME=${lib_NAME}\n" + "lib_OUTPUT_NAME=${lib_OUTPUT_NAME}\n" + "lib_TYPE=${lib_TYPE}\n" + "lib_SOURCES=${lib_SOURCES}\n" + "lib_DEFINES=${lib_DEFINES}\n" + "lib_INCLUDES=${lib_INCLUDES}\n" + "lib_COMPILE_FLAGS=${lib_COMPILE_FLAGS}\n" + "lib_LINK_FLAGS=${lib_LINK_FLAGS}\n" + "lib_OBJLIB_DEPS=${lib_OBJLIB_DEPS}\n" + "lib_LIB_DEPS=${lib_LIB_DEPS}\n" + "lib_PUBLIC_INCLUDES=${lib_PUBLIC_INCLUDES}\n" + "---------------------------------------\n") endif() if(NOT (lib_NAME AND lib_TYPE)) @@ -256,14 +290,24 @@ macro(draco_add_library) endif() add_library(${lib_NAME} ${lib_TYPE} ${lib_SOURCES}) + + target_compile_features(${lib_NAME} PUBLIC cxx_std_11) + + target_include_directories(${lib_NAME} PUBLIC $) + + if(BUILD_SHARED_LIBS) + # Enable PIC for all targets in shared configurations. + set_target_properties(${lib_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) + endif() + if(lib_SOURCES) draco_process_intrinsics_sources(TARGET ${lib_NAME} SOURCES ${lib_SOURCES}) endif() if(lib_OUTPUT_NAME) if(NOT (BUILD_SHARED_LIBS AND MSVC)) - set_target_properties(${lib_NAME} - PROPERTIES OUTPUT_NAME ${lib_OUTPUT_NAME}) + set_target_properties(${lib_NAME} PROPERTIES OUTPUT_NAME + ${lib_OUTPUT_NAME}) endif() endif() @@ -280,8 +324,8 @@ macro(draco_add_library) endif() if(lib_COMPILE_FLAGS OR DRACO_CXX_FLAGS) - target_compile_options(${lib_NAME} - PRIVATE ${lib_COMPILE_FLAGS} ${DRACO_CXX_FLAGS}) + target_compile_options(${lib_NAME} PRIVATE ${lib_COMPILE_FLAGS} + ${DRACO_CXX_FLAGS}) endif() if(lib_LINK_FLAGS) @@ -320,11 +364,12 @@ macro(draco_add_library) set_target_properties(${lib_NAME} PROPERTIES PREFIX "") endif() - # VERSION and SOVERSION as necessary - if(NOT lib_TYPE STREQUAL STATIC AND NOT lib_TYPE STREQUAL MODULE) - set_target_properties(${lib_NAME} PROPERTIES VERSION ${DRACO_VERSION}) - if(NOT MSVC) - set_target_properties(${lib_NAME} PROPERTIES SOVERSION ${DRACO_SOVERSION}) + if(NOT EMSCRIPTEN) + # VERSION and SOVERSION as necessary + if((lib_TYPE STREQUAL BUNDLE OR lib_TYPE STREQUAL SHARED) AND NOT MSVC) + set_target_properties( + ${lib_NAME} PROPERTIES VERSION ${DRACO_SOVERSION} + SOVERSION ${DRACO_SOVERSION_MAJOR}) endif() endif() diff --git a/contrib/draco/cmake/draco_test_config.h.cmake b/contrib/draco/cmake/draco_test_config.h.cmake index 77a574123..9bb174569 100644 --- a/contrib/draco/cmake/draco_test_config.h.cmake +++ b/contrib/draco/cmake/draco_test_config.h.cmake @@ -1,3 +1,17 @@ +// Copyright 2021 The Draco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #ifndef DRACO_TESTING_DRACO_TEST_CONFIG_H_ #define DRACO_TESTING_DRACO_TEST_CONFIG_H_ @@ -9,5 +23,6 @@ #define DRACO_TEST_DATA_DIR "${DRACO_TEST_DATA_DIR}" #define DRACO_TEST_TEMP_DIR "${DRACO_TEST_TEMP_DIR}" +#define DRACO_TEST_ROOT_DIR "${DRACO_TEST_ROOT_DIR}" #endif // DRACO_TESTING_DRACO_TEST_CONFIG_H_ diff --git a/contrib/draco/cmake/draco_tests.cmake b/contrib/draco/cmake/draco_tests.cmake index a6dfc5b57..1d905a969 100644 --- a/contrib/draco/cmake/draco_tests.cmake +++ b/contrib/draco/cmake/draco_tests.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_TESTS_CMAKE) return() endif() @@ -10,6 +24,13 @@ set(draco_factory_test_sources "${draco_src_root}/io/file_reader_factory_test.cc" "${draco_src_root}/io/file_writer_factory_test.cc") +list( + APPEND draco_test_common_sources + "${draco_src_root}/core/draco_test_base.h" + "${draco_src_root}/core/draco_test_utils.cc" + "${draco_src_root}/core/draco_test_utils.h" + "${draco_src_root}/core/status.cc") + list( APPEND draco_test_sources @@ -30,22 +51,23 @@ list( "${draco_src_root}/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc" "${draco_src_root}/compression/point_cloud/point_cloud_sequential_encoding_test.cc" "${draco_src_root}/core/buffer_bit_coding_test.cc" - "${draco_src_root}/core/draco_test_base.h" - "${draco_src_root}/core/draco_test_utils.cc" - "${draco_src_root}/core/draco_test_utils.h" "${draco_src_root}/core/math_utils_test.cc" "${draco_src_root}/core/quantization_utils_test.cc" "${draco_src_root}/core/status_test.cc" "${draco_src_root}/core/vector_d_test.cc" "${draco_src_root}/io/file_reader_test_common.h" "${draco_src_root}/io/file_utils_test.cc" + "${draco_src_root}/io/file_writer_utils_test.cc" "${draco_src_root}/io/stdio_file_reader_test.cc" "${draco_src_root}/io/stdio_file_writer_test.cc" "${draco_src_root}/io/obj_decoder_test.cc" "${draco_src_root}/io/obj_encoder_test.cc" "${draco_src_root}/io/ply_decoder_test.cc" "${draco_src_root}/io/ply_reader_test.cc" + "${draco_src_root}/io/stl_decoder_test.cc" + "${draco_src_root}/io/stl_encoder_test.cc" "${draco_src_root}/io/point_cloud_io_test.cc" + "${draco_src_root}/mesh/corner_table_test.cc" "${draco_src_root}/mesh/mesh_are_equivalent_test.cc" "${draco_src_root}/mesh/mesh_cleanup_test.cc" "${draco_src_root}/mesh/triangle_soup_mesh_builder_test.cc" @@ -54,47 +76,71 @@ list( "${draco_src_root}/point_cloud/point_cloud_builder_test.cc" "${draco_src_root}/point_cloud/point_cloud_test.cc") -list(APPEND draco_gtest_all - "${draco_root}/../googletest/googletest/src/gtest-all.cc") -list(APPEND draco_gtest_main - "${draco_root}/../googletest/googletest/src/gtest_main.cc") +if(DRACO_TRANSCODER_SUPPORTED) + list( + APPEND draco_test_sources + "${draco_src_root}/animation/animation_test.cc" + "${draco_src_root}/io/gltf_decoder_test.cc" + "${draco_src_root}/io/gltf_encoder_test.cc" + "${draco_src_root}/io/gltf_utils_test.cc" + "${draco_src_root}/io/gltf_test_helper.cc" + "${draco_src_root}/io/gltf_test_helper.h" + "${draco_src_root}/io/scene_io_test.cc" + "${draco_src_root}/io/texture_io_test.cc" + "${draco_src_root}/material/material_library_test.cc" + "${draco_src_root}/material/material_test.cc" + "${draco_src_root}/metadata/property_table_test.cc" + "${draco_src_root}/metadata/structural_metadata_test.cc" + "${draco_src_root}/scene/instance_array_test.cc" + "${draco_src_root}/scene/light_test.cc" + "${draco_src_root}/scene/mesh_group_test.cc" + "${draco_src_root}/scene/scene_test.cc" + "${draco_src_root}/scene/scene_are_equivalent_test.cc" + "${draco_src_root}/scene/scene_utils_test.cc" + "${draco_src_root}/scene/trs_matrix_test.cc" + "${draco_src_root}/texture/texture_library_test.cc" + "${draco_src_root}/texture/texture_map_test.cc" + "${draco_src_root}/texture/texture_transform_test.cc") + +endif() macro(draco_setup_test_targets) if(DRACO_TESTS) + draco_setup_googletest() + if(NOT (EXISTS ${draco_gtest_all} AND EXISTS ${draco_gtest_main})) - message(FATAL "googletest must be a sibling directory of ${draco_root}.") + message(FATAL_ERROR "googletest missing, run git submodule update --init") endif() list(APPEND draco_test_defines GTEST_HAS_PTHREAD=0) - draco_add_library(TEST - NAME - draco_gtest - TYPE - STATIC - SOURCES - ${draco_gtest_all} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths}) + draco_add_library( + TEST + NAME draco_test_common + TYPE STATIC + SOURCES ${draco_test_common_sources} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths}) - draco_add_library(TEST - NAME - draco_gtest_main - TYPE - STATIC - SOURCES - ${draco_gtest_main} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths}) + draco_add_library( + TEST + NAME draco_gtest + TYPE STATIC + SOURCES ${draco_gtest_all} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths}) + + draco_add_library( + TEST + NAME draco_gtest_main + TYPE STATIC + SOURCES ${draco_gtest_main} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths}) set(DRACO_TEST_DATA_DIR "${draco_root}/testdata") set(DRACO_TEST_TEMP_DIR "${draco_build}/draco_test_temp") + set(DRACO_TEST_ROOT_DIR "${draco_root}") file(MAKE_DIRECTORY "${DRACO_TEST_TEMP_DIR}") # Sets DRACO_TEST_DATA_DIR and DRACO_TEST_TEMP_DIR. @@ -102,32 +148,24 @@ macro(draco_setup_test_targets) "${draco_build}/testing/draco_test_config.h") # Create the test targets. - draco_add_executable(NAME - draco_tests - SOURCES - ${draco_test_sources} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths} - LIB_DEPS - draco_static - draco_gtest - draco_gtest_main) + draco_add_executable( + TEST + NAME draco_tests + SOURCES ${draco_test_sources} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths} + LIB_DEPS ${draco_dependency} draco_gtest draco_gtest_main + draco_test_common) + + draco_add_executable( + TEST + NAME draco_factory_tests + SOURCES ${draco_factory_test_sources} + DEFINES ${draco_defines} ${draco_test_defines} + INCLUDES ${draco_test_include_paths} + LIB_DEPS ${draco_dependency} draco_gtest draco_gtest_main + draco_test_common) + - draco_add_executable(NAME - draco_factory_tests - SOURCES - ${draco_factory_test_sources} - DEFINES - ${draco_defines} - ${draco_test_defines} - INCLUDES - ${draco_test_include_paths} - LIB_DEPS - draco_static - draco_gtest - draco_gtest_main) endif() endmacro() diff --git a/contrib/draco/cmake/draco_variables.cmake b/contrib/draco/cmake/draco_variables.cmake index 8dbc77a53..6d1b6a99d 100644 --- a/contrib/draco/cmake/draco_variables.cmake +++ b/contrib/draco/cmake/draco_variables.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_DRACO_VARIABLES_CMAKE_) return() endif() # DRACO_CMAKE_DRACO_VARIABLES_CMAKE_ @@ -14,8 +28,7 @@ macro(draco_variable_must_be_directory variable_name) if("${${variable_name}}" STREQUAL "") message( - FATAL_ERROR - "Empty variable ${variable_name} is required to build draco.") + FATAL_ERROR "Empty variable ${variable_name} is required to build draco.") endif() if(NOT IS_DIRECTORY "${${variable_name}}") @@ -44,11 +57,13 @@ macro(draco_dump_cmake_flag_variables) list(APPEND flag_variables "CMAKE_CXX_FLAGS_INIT" "CMAKE_CXX_FLAGS" "CMAKE_EXE_LINKER_FLAGS_INIT" "CMAKE_EXE_LINKER_FLAGS") if(CMAKE_BUILD_TYPE) - list(APPEND flag_variables "CMAKE_BUILD_TYPE" - "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}_INIT" - "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}" - "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}_INIT" - "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}") + list( + APPEND flag_variables + "CMAKE_BUILD_TYPE" + "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}_INIT" + "CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}" + "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}_INIT" + "CMAKE_EXE_LINKER_FLAGS_${CMAKE_BUILD_TYPE}") endif() foreach(flag_variable ${flag_variables}) message("${flag_variable}:${${flag_variable}}") diff --git a/contrib/draco/cmake/sanitizers.cmake b/contrib/draco/cmake/sanitizers.cmake deleted file mode 100644 index e720bc045..000000000 --- a/contrib/draco/cmake/sanitizers.cmake +++ /dev/null @@ -1,19 +0,0 @@ -if(DRACO_CMAKE_SANITIZERS_CMAKE_) - return() -endif() -set(DRACO_CMAKE_SANITIZERS_CMAKE_ 1) - -if(MSVC OR NOT SANITIZE) - return() -endif() - -include("${draco_root}/cmake/compiler_flags.cmake") - -string(TOLOWER ${SANITIZE} SANITIZE) - -# Require the sanitizer requested. -require_linker_flag("-fsanitize=${SANITIZE}") -require_compiler_flag("-fsanitize=${SANITIZE}" YES) - -# Make callstacks accurate. -require_compiler_flag("-fno-omit-frame-pointer -fno-optimize-sibling-calls" YES) diff --git a/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake b/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake index 87e0b4a45..a55da20fa 100644 --- a/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake +++ b/contrib/draco/cmake/toolchains/aarch64-linux-gnu.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_AARCH64_LINUX_GNU_CMAKE_) return() endif() # DRACO_CMAKE_TOOLCHAINS_AARCH64_LINUX_GNU_CMAKE_ diff --git a/contrib/draco/cmake/toolchains/android-ndk-common.cmake b/contrib/draco/cmake/toolchains/android-ndk-common.cmake index 5126d6e29..80396af48 100644 --- a/contrib/draco/cmake/toolchains/android-ndk-common.cmake +++ b/contrib/draco/cmake/toolchains/android-ndk-common.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ANDROID_NDK_COMMON_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/android.cmake b/contrib/draco/cmake/toolchains/android.cmake index b8f576d5e..ba50576b7 100644 --- a/contrib/draco/cmake/toolchains/android.cmake +++ b/contrib/draco/cmake/toolchains/android.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ANDROID_CMAKE_) return() endif() # DRACO_CMAKE_TOOLCHAINS_ANDROID_CMAKE_ @@ -16,9 +30,9 @@ if(NOT ANDROID_ABI) set(ANDROID_ABI arm64-v8a) endif() -# Force arm mode for 32-bit targets (instead of the default thumb) to improve -# performance. -if(NOT ANDROID_ARM_MODE) +# Force arm mode for 32-bit arm targets (instead of the default thumb) to +# improve performance. +if(ANDROID_ABI MATCHES "^armeabi" AND NOT ANDROID_ARM_MODE) set(ANDROID_ARM_MODE arm) endif() diff --git a/contrib/draco/cmake/toolchains/arm-ios-common.cmake b/contrib/draco/cmake/toolchains/arm-ios-common.cmake index 65326d1c2..fab54bb39 100644 --- a/contrib/draco/cmake/toolchains/arm-ios-common.cmake +++ b/contrib/draco/cmake/toolchains/arm-ios-common.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARM_IOS_COMMON_CMAKE_) return() endif() @@ -13,5 +27,3 @@ set(CMAKE_C_COMPILER clang) set(CMAKE_C_COMPILER_ARG1 "-arch ${CMAKE_SYSTEM_PROCESSOR}") set(CMAKE_CXX_COMPILER clang++) set(CMAKE_CXX_COMPILER_ARG1 "-arch ${CMAKE_SYSTEM_PROCESSOR}") - -# TODO(tomfinegan): Handle bit code embedding. diff --git a/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake b/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake index 6e45969e9..f1f83d67c 100644 --- a/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake +++ b/contrib/draco/cmake/toolchains/arm-linux-gnueabihf.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARM_LINUX_GNUEABIHF_CMAKE_) return() endif() # DRACO_CMAKE_TOOLCHAINS_ARM_LINUX_GNUEABIHF_CMAKE_ diff --git a/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake index 4b6d366f0..80d452f97 100644 --- a/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/arm64-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARM64_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/arm64-ios.cmake b/contrib/draco/cmake/toolchains/arm64-ios.cmake index c4ec7e3fa..5365d70f1 100644 --- a/contrib/draco/cmake/toolchains/arm64-ios.cmake +++ b/contrib/draco/cmake/toolchains/arm64-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARM64_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_ARM64_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake b/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake index 046ff0139..a332760b2 100644 --- a/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake +++ b/contrib/draco/cmake/toolchains/arm64-linux-gcc.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARM64_LINUX_GCC_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake index 80ee98b18..bedcc0cad 100644 --- a/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/armv7-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/armv7-ios.cmake b/contrib/draco/cmake/toolchains/armv7-ios.cmake index 8ddd6997b..43e208b1f 100644 --- a/contrib/draco/cmake/toolchains/armv7-ios.cmake +++ b/contrib/draco/cmake/toolchains/armv7-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_ARMV7_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake b/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake index 9c9472319..730a87f4b 100644 --- a/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake +++ b/contrib/draco/cmake/toolchains/armv7-linux-gcc.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7_LINUX_GCC_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/armv7s-ios.cmake b/contrib/draco/cmake/toolchains/armv7s-ios.cmake index b433025ba..472756117 100644 --- a/contrib/draco/cmake/toolchains/armv7s-ios.cmake +++ b/contrib/draco/cmake/toolchains/armv7s-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_ARMV7S_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_ARMV7S_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/i386-ios.cmake b/contrib/draco/cmake/toolchains/i386-ios.cmake index e9a105591..38989d225 100644 --- a/contrib/draco/cmake/toolchains/i386-ios.cmake +++ b/contrib/draco/cmake/toolchains/i386-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_i386_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_i386_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake index d43383640..6f63f2c31 100644 --- a/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/x86-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_X86_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake b/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake index d6fabeacc..7a630f4d4 100644 --- a/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake +++ b/contrib/draco/cmake/toolchains/x86_64-android-ndk-libcpp.cmake @@ -1,3 +1,17 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_X86_64_ANDROID_NDK_LIBCPP_CMAKE_) return() endif() diff --git a/contrib/draco/cmake/toolchains/x86_64-ios.cmake b/contrib/draco/cmake/toolchains/x86_64-ios.cmake index 4c50a72a2..6946ce410 100644 --- a/contrib/draco/cmake/toolchains/x86_64-ios.cmake +++ b/contrib/draco/cmake/toolchains/x86_64-ios.cmake @@ -1,10 +1,23 @@ +# Copyright 2021 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + if(DRACO_CMAKE_TOOLCHAINS_X86_64_IOS_CMAKE_) return() endif() set(DRACO_CMAKE_TOOLCHAINS_X86_64_IOS_CMAKE_ 1) if(XCODE) - # TODO(tomfinegan): Handle arm builds in Xcode. message(FATAL_ERROR "This toolchain does not support Xcode.") endif() diff --git a/contrib/draco/cmake/util.cmake b/contrib/draco/cmake/util.cmake deleted file mode 100644 index 813146a62..000000000 --- a/contrib/draco/cmake/util.cmake +++ /dev/null @@ -1,79 +0,0 @@ -if(DRACO_CMAKE_UTIL_CMAKE_) - return() -endif() -set(DRACO_CMAKE_UTIL_CMAKE_ 1) - -# Creates dummy source file in $draco_build_dir named $basename.$extension and -# returns the full path to the dummy source file via the $out_file_path -# parameter. -function(create_dummy_source_file basename extension out_file_path) - set(dummy_source_file "${draco_build_dir}/${basename}.${extension}") - file(WRITE "${dummy_source_file}.new" - "// Generated file. DO NOT EDIT!\n" - "// ${target_name} needs a ${extension} file to force link language, \n" - "// or to silence a harmless CMake warning: Ignore me.\n" - "void ${target_name}_dummy_function(void) {}\n") - - # Will replace ${dummy_source_file} only if the file content has changed. - # This prevents forced Draco rebuilds after CMake runs. - configure_file("${dummy_source_file}.new" "${dummy_source_file}") - file(REMOVE "${dummy_source_file}.new") - - set(${out_file_path} ${dummy_source_file} PARENT_SCOPE) -endfunction() - -# Convenience function for adding a dummy source file to $target_name using -# $extension as the file extension. Wraps create_dummy_source_file(). -function(add_dummy_source_file_to_target target_name extension) - create_dummy_source_file("${target_name}" "${extension}" "dummy_source_file") - target_sources(${target_name} PRIVATE ${dummy_source_file}) -endfunction() - -# Extracts the version number from $version_file and returns it to the user via -# $version_string_out_var. This is achieved by finding the first instance of the -# kDracoVersion variable and then removing everything but the string literal -# assigned to the variable. Quotes and semicolon are stripped from the returned -# string. -function(extract_version_string version_file version_string_out_var) - file(STRINGS "${version_file}" draco_version REGEX "kDracoVersion") - list(GET draco_version 0 draco_version) - string(REPLACE "static const char kDracoVersion[] = " "" draco_version - "${draco_version}") - string(REPLACE ";" "" draco_version "${draco_version}") - string(REPLACE "\"" "" draco_version "${draco_version}") - set("${version_string_out_var}" "${draco_version}" PARENT_SCOPE) -endfunction() - -# Sets CMake compiler launcher to $launcher_name when $launcher_name is found in -# $PATH. Warns user about ignoring build flag $launcher_flag when $launcher_name -# is not found in $PATH. -function(set_compiler_launcher launcher_flag launcher_name) - find_program(launcher_path "${launcher_name}") - if(launcher_path) - set(CMAKE_C_COMPILER_LAUNCHER "${launcher_path}" PARENT_SCOPE) - set(CMAKE_CXX_COMPILER_LAUNCHER "${launcher_path}" PARENT_SCOPE) - message("--- Using ${launcher_name} as compiler launcher.") - else() - message( - WARNING "--- Cannot find ${launcher_name}, ${launcher_flag} ignored.") - endif() -endfunction() - -# Terminates CMake execution when $var_name is unset in the environment. Sets -# CMake variable to the value of the environment variable when the variable is -# present in the environment. -macro(require_variable var_name) - if("$ENV{${var_name}}" STREQUAL "") - message(FATAL_ERROR "${var_name} must be set in environment.") - endif() - set_variable_if_unset(${var_name} "") -endmacro() - -# Sets $var_name to $default_value if not already set. -macro(set_variable_if_unset var_name default_value) - if(NOT "$ENV{${var_name}}" STREQUAL "") - set(${var_name} $ENV{${var_name}}) - elseif(NOT ${var_name}) - set(${var_name} ${default_value}) - endif() -endmacro() diff --git a/contrib/draco/src/draco/animation/animation.cc b/contrib/draco/src/draco/animation/animation.cc new file mode 100644 index 000000000..471cf2942 --- /dev/null +++ b/contrib/draco/src/draco/animation/animation.cc @@ -0,0 +1,47 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/animation/animation.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void Animation::Copy(const Animation &src) { + name_ = src.name_; + channels_.clear(); + for (int i = 0; i < src.NumChannels(); ++i) { + std::unique_ptr new_channel(new AnimationChannel()); + new_channel->Copy(*src.GetChannel(i)); + channels_.push_back(std::move(new_channel)); + } + + samplers_.clear(); + for (int i = 0; i < src.NumSamplers(); ++i) { + std::unique_ptr new_sampler(new AnimationSampler()); + new_sampler->Copy(*src.GetSampler(i)); + samplers_.push_back(std::move(new_sampler)); + } + + node_animation_data_.clear(); + for (int i = 0; i < src.NumNodeAnimationData(); ++i) { + std::unique_ptr new_data(new NodeAnimationData()); + new_data->Copy(*src.GetNodeAnimationData(i)); + node_animation_data_.push_back(std::move(new_data)); + } +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/animation/animation.h b/contrib/draco/src/draco/animation/animation.h new file mode 100644 index 000000000..3713f9886 --- /dev/null +++ b/contrib/draco/src/draco/animation/animation.h @@ -0,0 +1,149 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_ANIMATION_ANIMATION_H_ +#define DRACO_ANIMATION_ANIMATION_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/animation/node_animation_data.h" +#include "draco/core/status.h" + +namespace draco { + +// Struct to hold information about an animation's sampler. +struct AnimationSampler { + enum class SamplerInterpolation { LINEAR, STEP, CUBICSPLINE }; + + static std::string InterpolationToString(SamplerInterpolation value) { + switch (value) { + case SamplerInterpolation::STEP: + return "STEP"; + case SamplerInterpolation::CUBICSPLINE: + return "CUBICSPLINE"; + default: + return "LINEAR"; + } + } + + AnimationSampler() + : input_index(-1), + interpolation_type(SamplerInterpolation::LINEAR), + output_index(-1) {} + + void Copy(const AnimationSampler &src) { + input_index = src.input_index; + interpolation_type = src.interpolation_type; + output_index = src.output_index; + } + + int input_index; + SamplerInterpolation interpolation_type; + int output_index; +}; + +// Struct to hold information about an animation's channel. +struct AnimationChannel { + enum class ChannelTransformation { TRANSLATION, ROTATION, SCALE, WEIGHTS }; + + static std::string TransformationToString(ChannelTransformation value) { + switch (value) { + case ChannelTransformation::ROTATION: + return "rotation"; + case ChannelTransformation::SCALE: + return "scale"; + case ChannelTransformation::WEIGHTS: + return "weights"; + default: + return "translation"; + } + } + + AnimationChannel() + : target_index(-1), + transformation_type(ChannelTransformation::TRANSLATION), + sampler_index(-1) {} + + void Copy(const AnimationChannel &src) { + target_index = src.target_index; + transformation_type = src.transformation_type; + sampler_index = src.sampler_index; + } + + int target_index; + ChannelTransformation transformation_type; + int sampler_index; +}; + +// This class is used to hold data and information of glTF animations. +class Animation { + public: + Animation() {} + + void Copy(const Animation &src); + + const std::string &GetName() const { return name_; } + void SetName(const std::string &name) { name_ = name; } + + // Returns the number of channels in an animation. + int NumChannels() const { return channels_.size(); } + // Returns the number of samplers in an animation. + int NumSamplers() const { return samplers_.size(); } + // Returns the number of accessors in an animation. + int NumNodeAnimationData() const { return node_animation_data_.size(); } + + // Returns a channel in the animation. + AnimationChannel *GetChannel(int index) { return channels_[index].get(); } + const AnimationChannel *GetChannel(int index) const { + return channels_[index].get(); + } + // Returns a sampler in the animation. + AnimationSampler *GetSampler(int index) { return samplers_[index].get(); } + const AnimationSampler *GetSampler(int index) const { + return samplers_[index].get(); + } + // Returns an accessor in the animation. + NodeAnimationData *GetNodeAnimationData(int index) { + return node_animation_data_[index].get(); + } + const NodeAnimationData *GetNodeAnimationData(int index) const { + return node_animation_data_[index].get(); + } + + void AddNodeAnimationData( + std::unique_ptr node_animation_data) { + node_animation_data_.push_back(std::move(node_animation_data)); + } + void AddSampler(std::unique_ptr sampler) { + samplers_.push_back(std::move(sampler)); + } + void AddChannel(std::unique_ptr channel) { + channels_.push_back(std::move(channel)); + } + + private: + std::string name_; + std::vector> samplers_; + std::vector> channels_; + std::vector> node_animation_data_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_ANIMATION_ANIMATION_H_ diff --git a/contrib/draco/src/draco/animation/animation_test.cc b/contrib/draco/src/draco/animation/animation_test.cc new file mode 100644 index 000000000..473938bca --- /dev/null +++ b/contrib/draco/src/draco/animation/animation_test.cc @@ -0,0 +1,71 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/animation/animation.h" + +#include "draco/core/draco_test_base.h" +#include "draco/draco_features.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST(AnimationTest, TestCopy) { + // Test copying of animation data. + draco::Animation src_anim; + ASSERT_TRUE(src_anim.GetName().empty()); + src_anim.SetName("Walking"); + ASSERT_EQ(src_anim.GetName(), "Walking"); + + std::unique_ptr src_sampler_0( + new draco::AnimationSampler()); + src_sampler_0->interpolation_type = + draco::AnimationSampler::SamplerInterpolation::CUBICSPLINE; + std::unique_ptr src_sampler_1( + new draco::AnimationSampler()); + src_sampler_1->Copy(*src_sampler_0); + + ASSERT_EQ(src_sampler_0->interpolation_type, + src_sampler_1->interpolation_type); + + src_sampler_1->interpolation_type = + draco::AnimationSampler::SamplerInterpolation::STEP; + + src_anim.AddSampler(std::move(src_sampler_0)); + src_anim.AddSampler(std::move(src_sampler_1)); + ASSERT_EQ(src_anim.NumSamplers(), 2); + + std::unique_ptr src_channel( + new draco::AnimationChannel()); + src_channel->transformation_type = + draco::AnimationChannel::ChannelTransformation::WEIGHTS; + src_anim.AddChannel(std::move(src_channel)); + ASSERT_EQ(src_anim.NumChannels(), 1); + + draco::Animation dst_anim; + dst_anim.Copy(src_anim); + + ASSERT_EQ(dst_anim.GetName(), src_anim.GetName()); + ASSERT_EQ(dst_anim.NumSamplers(), 2); + ASSERT_EQ(dst_anim.NumChannels(), 1); + + ASSERT_EQ(dst_anim.GetSampler(0)->interpolation_type, + src_anim.GetSampler(0)->interpolation_type); + ASSERT_EQ(dst_anim.GetSampler(1)->interpolation_type, + src_anim.GetSampler(1)->interpolation_type); + ASSERT_EQ(dst_anim.GetChannel(0)->transformation_type, + src_anim.GetChannel(0)->transformation_type); +} +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc b/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc index 4a6491f9d..fcd0eaa6f 100644 --- a/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc +++ b/contrib/draco/src/draco/animation/keyframe_animation_encoding_test.cc @@ -26,8 +26,9 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { bool CreateAndAddTimestamps(int32_t num_frames) { timestamps_.resize(num_frames); - for (int i = 0; i < timestamps_.size(); ++i) + for (int i = 0; i < timestamps_.size(); ++i) { timestamps_[i] = static_cast(i); + } return keyframe_animation_.SetTimestamps(timestamps_); } @@ -35,8 +36,9 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { uint32_t num_components) { // Create and add animation data with. animation_data_.resize(num_frames * num_components); - for (int i = 0; i < animation_data_.size(); ++i) + for (int i = 0; i < animation_data_.size(); ++i) { animation_data_[i] = static_cast(i); + } return keyframe_animation_.AddKeyframes(draco::DT_FLOAT32, num_components, animation_data_); } @@ -49,7 +51,7 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { ASSERT_EQ(animation0.num_animations(), animation1.num_animations()); if (quantized) { - // TODO(hemmer) : Add test for stable quantization. + // TODO(b/199760123) : Add test for stable quantization. // Quantization will result in slightly different values. // Skip comparing values. return; @@ -109,9 +111,8 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { } } - ASSERT_TRUE( - encoder.EncodeKeyframeAnimation(keyframe_animation_, options, &buffer) - .ok()); + DRACO_ASSERT_OK( + encoder.EncodeKeyframeAnimation(keyframe_animation_, options, &buffer)); draco::DecoderBuffer dec_decoder; draco::KeyframeAnimationDecoder decoder; @@ -122,8 +123,8 @@ class KeyframeAnimationEncodingTest : public ::testing::Test { std::unique_ptr decoded_animation( new KeyframeAnimation()); DecoderOptions dec_options; - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_animation.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_animation.get())); // Verify if animation before and after compression is identical. CompareAnimationData(keyframe_animation_, diff --git a/contrib/draco/src/draco/animation/keyframe_animation_test.cc b/contrib/draco/src/draco/animation/keyframe_animation_test.cc index bc92b25ff..94566972b 100644 --- a/contrib/draco/src/draco/animation/keyframe_animation_test.cc +++ b/contrib/draco/src/draco/animation/keyframe_animation_test.cc @@ -24,8 +24,9 @@ class KeyframeAnimationTest : public ::testing::Test { bool CreateAndAddTimestamps(int32_t num_frames) { timestamps_.resize(num_frames); - for (int i = 0; i < timestamps_.size(); ++i) + for (int i = 0; i < timestamps_.size(); ++i) { timestamps_[i] = static_cast(i); + } return keyframe_animation_.SetTimestamps(timestamps_); } @@ -33,8 +34,9 @@ class KeyframeAnimationTest : public ::testing::Test { uint32_t num_components) { // Create and add animation data with. animation_data_.resize(num_frames * num_components); - for (int i = 0; i < animation_data_.size(); ++i) + for (int i = 0; i < animation_data_.size(); ++i) { animation_data_[i] = static_cast(i); + } return keyframe_animation_.AddKeyframes(draco::DT_FLOAT32, num_components, animation_data_); } diff --git a/contrib/draco/src/draco/animation/node_animation_data.h b/contrib/draco/src/draco/animation/node_animation_data.h new file mode 100644 index 000000000..7799e3376 --- /dev/null +++ b/contrib/draco/src/draco/animation/node_animation_data.h @@ -0,0 +1,150 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_ANIMATION_NODE_ANIMATION_DATA_H_ +#define DRACO_ANIMATION_NODE_ANIMATION_DATA_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include "draco/core/hash_utils.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" + +namespace draco { + +// This class is used to store information and data for animations that only +// affect the nodes. +// TODO(fgalligan): Think about changing the name of this class now that Skin +// is using it. +class NodeAnimationData { + public: + enum class Type { SCALAR, VEC3, VEC4, MAT4 }; + + NodeAnimationData() : type_(Type::SCALAR), count_(0), normalized_(false) {} + + void Copy(const NodeAnimationData &src) { + type_ = src.type_; + count_ = src.count_; + normalized_ = src.normalized_; + data_ = src.data_; + } + + Type type() const { return type_; } + int count() const { return count_; } + bool normalized() const { return normalized_; } + + std::vector *GetMutableData() { return &data_; } + const std::vector *GetData() const { return &data_; } + + void SetType(Type type) { type_ = type; } + void SetCount(int count) { count_ = count; } + void SetNormalized(bool normalized) { normalized_ = normalized; } + + int ComponentSize() const { return sizeof(float); } + int NumComponents() const { + switch (type_) { + case Type::SCALAR: + return 1; + case Type::VEC3: + return 3; + case Type::MAT4: + return 16; + default: + return 4; + } + } + + std::string TypeAsString() const { + switch (type_) { + case Type::SCALAR: + return "SCALAR"; + case Type::VEC3: + return "VEC3"; + case Type::MAT4: + return "MAT4"; + default: + return "VEC4"; + } + } + + bool operator==(const NodeAnimationData &nad) const { + return type_ == nad.type_ && count_ == nad.count_ && + normalized_ == nad.normalized_ && data_ == nad.data_; + } + + private: + Type type_; + int count_; + bool normalized_; + std::vector data_; +}; + +// Wrapper class for hashing NodeAnimationData. When using different containers, +// this class is preferable instead of copying the data in NodeAnimationData +// every time. +class NodeAnimationDataHash { + public: + NodeAnimationDataHash() = delete; + NodeAnimationDataHash &operator=(const NodeAnimationDataHash &) = delete; + NodeAnimationDataHash(NodeAnimationDataHash &&) = delete; + NodeAnimationDataHash &operator=(NodeAnimationDataHash &&) = delete; + + explicit NodeAnimationDataHash(const NodeAnimationData *nad) + : node_animation_data_(nad) { + hash_ = NodeAnimationDataHash::HashNodeAnimationData(*node_animation_data_); + } + + NodeAnimationDataHash(const NodeAnimationDataHash &nadh) { + node_animation_data_ = nadh.node_animation_data_; + hash_ = nadh.hash_; + } + + bool operator==(const NodeAnimationDataHash &nadh) const { + return *node_animation_data_ == *nadh.node_animation_data_; + } + + struct Hash { + size_t operator()(const NodeAnimationDataHash &nadh) const { + return nadh.hash_; + } + }; + + const NodeAnimationData *GetNodeAnimationData() { + return node_animation_data_; + } + + private: + // Returns a hash of |nad|. + static size_t HashNodeAnimationData(const NodeAnimationData &nad) { + size_t hash = 79; // Magic number. + hash = HashCombine(static_cast(nad.type()), hash); + hash = HashCombine(nad.count(), hash); + hash = HashCombine(nad.normalized(), hash); + const uint64_t data_hash = + FingerprintString(reinterpret_cast(nad.GetData()->data()), + nad.GetData()->size() * sizeof(float)); + hash = HashCombine(data_hash, hash); + return hash; + } + + const NodeAnimationData *node_animation_data_; + size_t hash_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_ANIMATION_NODE_ANIMATION_DATA_H_ diff --git a/contrib/draco/src/draco/animation/skin.cc b/contrib/draco/src/draco/animation/skin.cc new file mode 100644 index 000000000..f232978c2 --- /dev/null +++ b/contrib/draco/src/draco/animation/skin.cc @@ -0,0 +1,29 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/animation/skin.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void Skin::Copy(const Skin &s) { + inverse_bind_matrices_.Copy(s.GetInverseBindMatrices()); + joints_ = s.GetJoints(); + joint_root_index_ = s.GetJointRoot(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/animation/skin.h b/contrib/draco/src/draco/animation/skin.h new file mode 100644 index 000000000..81ca997eb --- /dev/null +++ b/contrib/draco/src/draco/animation/skin.h @@ -0,0 +1,64 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_ANIMATION_SKIN_H_ +#define DRACO_ANIMATION_SKIN_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include + +#include "draco/animation/node_animation_data.h" +#include "draco/scene/scene_indices.h" + +namespace draco { + +// This class is used to store information on animation skins. +class Skin { + public: + Skin() : joint_root_index_(-1) {} + + void Copy(const Skin &s); + + NodeAnimationData &GetInverseBindMatrices() { return inverse_bind_matrices_; } + const NodeAnimationData &GetInverseBindMatrices() const { + return inverse_bind_matrices_; + } + + int AddJoint(SceneNodeIndex index) { + joints_.push_back(index); + return joints_.size() - 1; + } + int NumJoints() const { return joints_.size(); } + SceneNodeIndex GetJoint(int index) const { return joints_[index]; } + SceneNodeIndex &GetJoint(int index) { return joints_[index]; } + const std::vector &GetJoints() const { return joints_; } + + void SetJointRoot(SceneNodeIndex index) { joint_root_index_ = index; } + SceneNodeIndex GetJointRoot() const { return joint_root_index_; } + + private: + NodeAnimationData inverse_bind_matrices_; + + // List of node indices that make up the joint hierarchy. + std::vector joints_; + SceneNodeIndex joint_root_index_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_ANIMATION_SKIN_H_ diff --git a/contrib/draco/src/draco/attributes/attribute_transform.cc b/contrib/draco/src/draco/attributes/attribute_transform.cc index 174e6b822..fb2ed1829 100644 --- a/contrib/draco/src/draco/attributes/attribute_transform.cc +++ b/contrib/draco/src/draco/attributes/attribute_transform.cc @@ -28,12 +28,13 @@ std::unique_ptr AttributeTransform::InitTransformedAttribute( const PointAttribute &src_attribute, int num_entries) { const int num_components = GetTransformedNumComponents(src_attribute); const DataType dt = GetTransformedDataType(src_attribute); - GeometryAttribute va; - va.Init(src_attribute.attribute_type(), nullptr, num_components, dt, false, + GeometryAttribute ga; + ga.Init(src_attribute.attribute_type(), nullptr, num_components, dt, false, num_components * DataTypeLength(dt), 0); - std::unique_ptr transformed_attribute(new PointAttribute(va)); + std::unique_ptr transformed_attribute(new PointAttribute(ga)); transformed_attribute->Reset(num_entries); transformed_attribute->SetIdentityMapping(); + transformed_attribute->set_unique_id(src_attribute.unique_id()); return transformed_attribute; } diff --git a/contrib/draco/src/draco/attributes/geometry_attribute.cc b/contrib/draco/src/draco/attributes/geometry_attribute.cc index b62478426..141130f43 100644 --- a/contrib/draco/src/draco/attributes/geometry_attribute.cc +++ b/contrib/draco/src/draco/attributes/geometry_attribute.cc @@ -26,7 +26,7 @@ GeometryAttribute::GeometryAttribute() unique_id_(0) {} void GeometryAttribute::Init(GeometryAttribute::Type attribute_type, - DataBuffer *buffer, int8_t num_components, + DataBuffer *buffer, uint8_t num_components, DataType data_type, bool normalized, int64_t byte_stride, int64_t byte_offset) { buffer_ = buffer; diff --git a/contrib/draco/src/draco/attributes/geometry_attribute.h b/contrib/draco/src/draco/attributes/geometry_attribute.h index f4d099b1b..28f743fa0 100644 --- a/contrib/draco/src/draco/attributes/geometry_attribute.h +++ b/contrib/draco/src/draco/attributes/geometry_attribute.h @@ -15,12 +15,18 @@ #ifndef DRACO_ATTRIBUTES_GEOMETRY_ATTRIBUTE_H_ #define DRACO_ATTRIBUTES_GEOMETRY_ATTRIBUTE_H_ +#include #include +#include #include #include "draco/attributes/geometry_indices.h" #include "draco/core/data_buffer.h" #include "draco/core/hash_utils.h" +#include "draco/draco_features.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" +#endif namespace draco { @@ -51,6 +57,16 @@ class GeometryAttribute { // predefined use case. Such attributes are often used for a shader specific // data. GENERIC, +#ifdef DRACO_TRANSCODER_SUPPORTED + // TODO(ostava): Adding a new attribute would be bit-stream change for GLTF. + // Older decoders wouldn't know what to do with this attribute type. This + // should be open-sourced only when we are ready to increase our bit-stream + // version. + TANGENT, + MATERIAL, + JOINTS, + WEIGHTS, +#endif // Total number of different attribute types. // Always keep behind all named attributes. NAMED_ATTRIBUTES_COUNT, @@ -58,7 +74,7 @@ class GeometryAttribute { GeometryAttribute(); // Initializes and enables the attribute. - void Init(Type attribute_type, DataBuffer *buffer, int8_t num_components, + void Init(Type attribute_type, DataBuffer *buffer, uint8_t num_components, DataType data_type, bool normalized, int64_t byte_stride, int64_t byte_offset); bool IsValid() const { return buffer_ != nullptr; } @@ -129,6 +145,17 @@ class GeometryAttribute { buffer_->Write(byte_pos, value, byte_stride()); } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Sets a value of an attribute entry. The input |value| must have + // |input_num_components| entries and it will be automatically converted to + // the internal format used by the geometry attribute. If the conversion is + // not possible, an error status will be returned. + template + Status ConvertAndSetAttributeValue(AttributeValueIndex avi, + int input_num_components, + const InputT *value); +#endif + // DEPRECATED: Use // ConvertValue(AttributeValueIndex att_id, // int out_num_components, @@ -233,10 +260,11 @@ class GeometryAttribute { // Returns the number of components that are stored for each entry. // For position attribute this is usually three (x,y,z), // while texture coordinates have two components (u,v). - int8_t num_components() const { return num_components_; } + uint8_t num_components() const { return num_components_; } // Indicates whether the data type should be normalized before interpretation, // that is, it should be divided by the max value of the data type. bool normalized() const { return normalized_; } + void set_normalized(bool normalized) { normalized_ = normalized; } // The buffer storing the entire data of the attribute. const DataBuffer *buffer() const { return buffer_; } // Returns the number of bytes between two attribute entries, this is, at @@ -260,7 +288,7 @@ class GeometryAttribute { // T is the stored attribute data type. // OutT is the desired data type of the attribute. template - bool ConvertTypedValue(AttributeValueIndex att_id, int8_t out_num_components, + bool ConvertTypedValue(AttributeValueIndex att_id, uint8_t out_num_components, OutT *out_value) const { const uint8_t *src_address = GetAddress(att_id); @@ -270,29 +298,10 @@ class GeometryAttribute { return false; } const T in_value = *reinterpret_cast(src_address); - - // Make sure the in_value fits within the range of values that OutT - // is able to represent. Perform the check only for integral types. - if (std::is_integral::value && std::is_integral::value) { - static constexpr OutT kOutMin = - std::is_signed::value ? std::numeric_limits::lowest() : 0; - if (in_value < kOutMin || in_value > std::numeric_limits::max()) { - return false; - } + if (!ConvertComponentValue(in_value, normalized_, + out_value + i)) { + return false; } - - out_value[i] = static_cast(in_value); - // When converting integer to floating point, normalize the value if - // necessary. - if (std::is_integral::value && std::is_floating_point::value && - normalized_) { - out_value[i] /= static_cast(std::numeric_limits::max()); - } - // TODO(ostava): Add handling of normalized attributes when converting - // between different integer representations. If the attribute is - // normalized, integer values should be converted as if they represent 0-1 - // range. E.g. when we convert uint16 to uint8, the range <0, 2^16 - 1> - // should be converted to range <0, 2^8 - 1>. src_address += sizeof(T); } // Fill empty data for unused output components if needed. @@ -302,12 +311,128 @@ class GeometryAttribute { return true; } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Function that converts input |value| from type T to the internal attribute + // representation defined by OutT and |num_components_|. + template + Status ConvertAndSetAttributeTypedValue(AttributeValueIndex avi, + int8_t input_num_components, + const T *value) { + uint8_t *address = GetAddress(avi); + + // Convert all components available in both the original and output formats. + for (int i = 0; i < num_components_; ++i) { + if (!IsAddressValid(address)) { + return ErrorStatus("GeometryAttribute: Invalid address."); + } + OutT *const out_value = reinterpret_cast(address); + if (i < input_num_components) { + if (!ConvertComponentValue(*(value + i), normalized_, + out_value)) { + return ErrorStatus( + "GeometryAttribute: Failed to convert component value."); + } + } else { + *out_value = static_cast(0); + } + address += sizeof(OutT); + } + return OkStatus(); + } +#endif // DRACO_TRANSCODER_SUPPORTED + + // Converts |in_value| of type T into |out_value| of type OutT. If + // |normalized| is true, any conversion between floating point and integer + // values will be treating integers as normalized types (the entire integer + // range will be used to represent 0-1 floating point range). + template + static bool ConvertComponentValue(const T &in_value, bool normalized, + OutT *out_value) { + // Make sure the |in_value| can be represented as an integral type OutT. + if (std::is_integral::value) { + // Make sure the |in_value| fits within the range of values that OutT + // is able to represent. Perform the check only for integral types. + if (!std::is_same::value && std::is_integral::value) { + static constexpr OutT kOutMin = + std::is_signed::value ? std::numeric_limits::min() : 0; + if (in_value < kOutMin || in_value > std::numeric_limits::max()) { + return false; + } + } + + // Check conversion of floating point |in_value| to integral value OutT. + if (std::is_floating_point::value) { + // Make sure the floating point |in_value| is not NaN and not Inf as + // integral type OutT is unable to represent these values. + if (sizeof(in_value) > sizeof(double)) { + if (std::isnan(static_cast(in_value)) || + std::isinf(static_cast(in_value))) { + return false; + } + } else if (sizeof(in_value) > sizeof(float)) { + if (std::isnan(static_cast(in_value)) || + std::isinf(static_cast(in_value))) { + return false; + } + } else { + if (std::isnan(static_cast(in_value)) || + std::isinf(static_cast(in_value))) { + return false; + } + } + + // Make sure the floating point |in_value| fits within the range of + // values that integral type OutT is able to represent. + if (in_value < std::numeric_limits::min() || + in_value >= std::numeric_limits::max()) { + return false; + } + } + } + + if (std::is_integral::value && std::is_floating_point::value && + normalized) { + // When converting integer to floating point, normalize the value if + // necessary. + *out_value = static_cast(in_value); + *out_value /= static_cast(std::numeric_limits::max()); + } else if (std::is_floating_point::value && + std::is_integral::value && normalized) { + // Converting from floating point to a normalized integer. + if (in_value > 1 || in_value < 0) { + // Normalized float values need to be between 0 and 1. + return false; + } + // TODO(ostava): Consider allowing float to normalized integer conversion + // for 64-bit integer types. Currently it doesn't work because we don't + // have a floating point type that could store all 64 bit integers. + if (sizeof(OutT) > 4) { + return false; + } + // Expand the float to the range of the output integer and round it to the + // nearest representable value. Use doubles for the math to ensure the + // integer values are represented properly during the conversion process. + *out_value = static_cast(std::floor( + in_value * static_cast(std::numeric_limits::max()) + + 0.5)); + } else { + *out_value = static_cast(in_value); + } + + // TODO(ostava): Add handling of normalized attributes when converting + // between different integer representations. If the attribute is + // normalized, integer values should be converted as if they represent 0-1 + // range. E.g. when we convert uint16 to uint8, the range <0, 2^16 - 1> + // should be converted to range <0, 2^8 - 1>. + return true; + } + DataBuffer *buffer_; // The buffer descriptor is stored at the time the buffer is attached to this // attribute. The purpose is to detect if any changes happened to the buffer // since the time it was attached. DataBufferDescriptor buffer_descriptor_; - int8_t num_components_; + uint8_t num_components_; DataType data_type_; bool normalized_; int64_t byte_stride_; @@ -323,6 +448,54 @@ class GeometryAttribute { friend struct GeometryAttributeHasher; }; +#ifdef DRACO_TRANSCODER_SUPPORTED +template +Status GeometryAttribute::ConvertAndSetAttributeValue(AttributeValueIndex avi, + int input_num_components, + const InputT *value) { + switch (this->data_type()) { + case DT_INT8: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT8: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_INT16: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT16: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_INT32: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT32: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_INT64: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_UINT64: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_FLOAT32: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_FLOAT64: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + case DT_BOOL: + return ConvertAndSetAttributeTypedValue( + avi, input_num_components, value); + default: + break; + } + return ErrorStatus( + "GeometryAttribute::SetAndConvertAttributeValue: Unsupported " + "attribute type."); +} +#endif + // Hashing support // Function object for using Attribute as a hash key. diff --git a/contrib/draco/src/draco/attributes/point_attribute.cc b/contrib/draco/src/draco/attributes/point_attribute.cc index b28f860c1..e54ab5427 100644 --- a/contrib/draco/src/draco/attributes/point_attribute.cc +++ b/contrib/draco/src/draco/attributes/point_attribute.cc @@ -222,4 +222,47 @@ AttributeValueIndex::ValueType PointAttribute::DeduplicateFormattedValues( } #endif +#ifdef DRACO_TRANSCODER_SUPPORTED +void PointAttribute::RemoveUnusedValues() { + if (is_mapping_identity()) { + return; // For identity mapping, all values are always used. + } + // For explicit mapping we need to check if any point is mapped to a value. + // If not we can delete the value. + IndexTypeVector is_value_used(size(), false); + int num_used_values = 0; + for (PointIndex pi(0); pi < indices_map_.size(); ++pi) { + const AttributeValueIndex avi = indices_map_[pi]; + if (!is_value_used[avi]) { + is_value_used[avi] = true; + num_used_values++; + } + } + if (num_used_values == size()) { + return; // All values are used. + } + + // Remap the values and update the point to value mapping. + IndexTypeVector + old_to_new_value_map(size(), kInvalidAttributeValueIndex); + AttributeValueIndex new_avi(0); + for (AttributeValueIndex avi(0); avi < size(); ++avi) { + if (!is_value_used[avi]) { + continue; + } + if (avi != new_avi) { + SetAttributeValue(new_avi, GetAddress(avi)); + } + old_to_new_value_map[avi] = new_avi++; + } + + // Remap all points to the new attribute values. + for (PointIndex pi(0); pi < indices_map_.size(); ++pi) { + indices_map_[pi] = old_to_new_value_map[indices_map_[pi]]; + } + + num_unique_entries_ = num_used_values; +} +#endif + } // namespace draco diff --git a/contrib/draco/src/draco/attributes/point_attribute.h b/contrib/draco/src/draco/attributes/point_attribute.h index ee3662031..d55c50c8a 100644 --- a/contrib/draco/src/draco/attributes/point_attribute.h +++ b/contrib/draco/src/draco/attributes/point_attribute.h @@ -133,6 +133,12 @@ class PointAttribute : public GeometryAttribute { return attribute_transform_data_.get(); } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Removes unused values from the attribute. Value is unused when no point + // is mapped to the value. Only applicable when the mapping is not identity. + void RemoveUnusedValues(); +#endif + private: #ifdef DRACO_ATTRIBUTE_VALUES_DEDUPLICATION_SUPPORTED template diff --git a/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc b/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc index 797c62f30..480e3ff34 100644 --- a/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc +++ b/contrib/draco/src/draco/compression/attributes/attributes_encoder.cc @@ -15,14 +15,16 @@ #include "draco/compression/attributes/attributes_encoder.h" #include "draco/core/varint_encoding.h" +#include "draco/draco_features.h" namespace draco { AttributesEncoder::AttributesEncoder() : point_cloud_encoder_(nullptr), point_cloud_(nullptr) {} -AttributesEncoder::AttributesEncoder(int att_id) : AttributesEncoder() { - AddAttributeId(att_id); +AttributesEncoder::AttributesEncoder(int point_attrib_id) + : AttributesEncoder() { + AddAttributeId(point_attrib_id); } bool AttributesEncoder::Init(PointCloudEncoder *encoder, const PointCloud *pc) { @@ -37,7 +39,15 @@ bool AttributesEncoder::EncodeAttributesEncoderData(EncoderBuffer *out_buffer) { for (uint32_t i = 0; i < num_attributes(); ++i) { const int32_t att_id = point_attribute_ids_[i]; const PointAttribute *const pa = point_cloud_->attribute(att_id); - out_buffer->Encode(static_cast(pa->attribute_type())); + GeometryAttribute::Type type = pa->attribute_type(); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Attribute types TANGENT, MATERIAL, JOINTS, and WEIGHTS are not supported + // in the official bitstream. They will be encoded as GENERIC. + if (type > GeometryAttribute::GENERIC) { + type = GeometryAttribute::GENERIC; + } +#endif + out_buffer->Encode(static_cast(type)); out_buffer->Encode(static_cast(pa->data_type())); out_buffer->Encode(static_cast(pa->num_components())); out_buffer->Encode(static_cast(pa->normalized())); diff --git a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc index e4d53485d..51c41cf7a 100644 --- a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc +++ b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.cc @@ -72,16 +72,19 @@ class PointAttributeVectorOutputIterator { Self &operator*() { return *this; } // Still needed in some cases. - // TODO(hemmer): remove. + // TODO(b/199760123): Remove. // hardcoded to 3 based on legacy usage. const Self &operator=(const VectorD &val) { DRACO_DCHECK_EQ(attributes_.size(), 1); // Expect only ONE attribute. AttributeTuple &att = attributes_[0]; PointAttribute *attribute = std::get<0>(att); + const AttributeValueIndex avi = attribute->mapped_index(point_id_); + if (avi >= static_cast(attribute->size())) { + return *this; + } const uint32_t &offset = std::get<1>(att); DRACO_DCHECK_EQ(offset, 0); // expected to be zero - attribute->SetAttributeValue(attribute->mapped_index(point_id_), - &val[0] + offset); + attribute->SetAttributeValue(avi, &val[0] + offset); return *this; } // Additional operator taking std::vector as argument. @@ -89,6 +92,10 @@ class PointAttributeVectorOutputIterator { for (auto index = 0; index < attributes_.size(); index++) { AttributeTuple &att = attributes_[index]; PointAttribute *attribute = std::get<0>(att); + const AttributeValueIndex avi = attribute->mapped_index(point_id_); + if (avi >= static_cast(attribute->size())) { + return *this; + } const uint32_t &offset = std::get<1>(att); const uint32_t &data_size = std::get<3>(att); const uint32_t &num_components = std::get<4>(att); @@ -103,10 +110,6 @@ class PointAttributeVectorOutputIterator { // redirect to copied data data_source = reinterpret_cast(data_); } - const AttributeValueIndex avi = attribute->mapped_index(point_id_); - if (avi >= static_cast(attribute->size())) { - return *this; - } attribute->SetAttributeValue(avi, data_source); } return *this; @@ -195,54 +198,55 @@ bool KdTreeAttributesDecoder::DecodePortableAttributes( data_size, num_components); total_dimensionality += num_components; } - PointAttributeVectorOutputIterator out_it(atts); + typedef PointAttributeVectorOutputIterator OutIt; + OutIt out_it(atts); switch (compression_level) { case 0: { - DynamicIntegerPointsKdTreeDecoder<0> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<0, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 1: { - DynamicIntegerPointsKdTreeDecoder<1> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<1, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 2: { - DynamicIntegerPointsKdTreeDecoder<2> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<2, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 3: { - DynamicIntegerPointsKdTreeDecoder<3> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<3, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 4: { - DynamicIntegerPointsKdTreeDecoder<4> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<4, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 5: { - DynamicIntegerPointsKdTreeDecoder<5> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<5, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; } case 6: { - DynamicIntegerPointsKdTreeDecoder<6> decoder(total_dimensionality); - if (!decoder.DecodePoints(in_buffer, out_it)) { + if (!DecodePoints<6, OutIt>(total_dimensionality, num_points, in_buffer, + &out_it)) { return false; } break; @@ -253,6 +257,19 @@ bool KdTreeAttributesDecoder::DecodePortableAttributes( return true; } +template +bool KdTreeAttributesDecoder::DecodePoints(int total_dimensionality, + int num_expected_points, + DecoderBuffer *in_buffer, + OutIteratorT *out_iterator) { + DynamicIntegerPointsKdTreeDecoder decoder(total_dimensionality); + if (!decoder.DecodePoints(in_buffer, *out_iterator, num_expected_points) || + decoder.num_decoded_points() != num_expected_points) { + return false; + } + return true; +} + bool KdTreeAttributesDecoder::DecodeDataNeededByPortableTransforms( DecoderBuffer *in_buffer) { if (in_buffer->bitstream_version() >= DRACO_BITSTREAM_VERSION(2, 3)) { @@ -336,6 +353,10 @@ bool KdTreeAttributesDecoder::DecodeDataNeededByPortableTransforms( return false; } if (method == KdTreeAttributesEncodingMethod::kKdTreeQuantizationEncoding) { + // This method only supports one attribute with exactly three components. + if (atts.size() != 1 || std::get<4>(atts[0]) != 3) { + return false; + } uint8_t compression_level = 0; if (!in_buffer->Decode(&compression_level)) { return false; @@ -376,7 +397,7 @@ bool KdTreeAttributesDecoder::DecodeDataNeededByPortableTransforms( GetDecoder()->point_cloud()->attribute(att_id); attr->Reset(num_points); attr->SetIdentityMapping(); - }; + } PointAttributeVectorOutputIterator out_it(atts); @@ -455,7 +476,11 @@ bool KdTreeAttributesDecoder::TransformAttributeBackToSignedType( att->GetValue(avi, &unsigned_val[0]); for (int c = 0; c < att->num_components(); ++c) { // Up-cast |unsigned_val| to int32_t to ensure we don't overflow it for - // smaller data types. + // smaller data types. But first check that the up-casting does not cause + // signed integer overflow. + if (unsigned_val[c] > std::numeric_limits::max()) { + return false; + } signed_val[c] = static_cast( static_cast(unsigned_val[c]) + min_signed_values_[num_processed_signed_components + c]); diff --git a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h index 87338d6b0..4af367a1a 100644 --- a/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h +++ b/contrib/draco/src/draco/compression/attributes/kd_tree_attributes_decoder.h @@ -31,6 +31,10 @@ class KdTreeAttributesDecoder : public AttributesDecoder { bool TransformAttributesToOriginalFormat() override; private: + template + bool DecodePoints(int total_dimensionality, int num_expected_points, + DecoderBuffer *in_buffer, OutIteratorT *out_iterator); + template bool TransformAttributeBackToSignedType(PointAttribute *att, int num_processed_signed_components); diff --git a/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h b/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h index 8a6f25b66..b717d0dbe 100644 --- a/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h +++ b/contrib/draco/src/draco/compression/attributes/normal_compression_utils.h @@ -61,7 +61,7 @@ class OctahedronToolBox { return false; } quantization_bits_ = q; - max_quantized_value_ = (1 << quantization_bits_) - 1; + max_quantized_value_ = (1u << quantization_bits_) - 1; max_value_ = max_quantized_value_ - 1; dequantization_scale_ = 2.f / max_value_; center_value_ = max_value_ / 2; @@ -208,7 +208,9 @@ class OctahedronToolBox { DRACO_DCHECK_LE(t, center_value_); DRACO_DCHECK_GE(s, -center_value_); DRACO_DCHECK_GE(t, -center_value_); - return std::abs(s) + std::abs(t) <= center_value_; + const uint32_t st = + static_cast(std::abs(s)) + static_cast(std::abs(t)); + return st <= center_value_; } void InvertDiamond(int32_t *s, int32_t *t) const { @@ -230,19 +232,29 @@ class OctahedronToolBox { sign_t = (*t > 0) ? 1 : -1; } - const int32_t corner_point_s = sign_s * center_value_; - const int32_t corner_point_t = sign_t * center_value_; - *s = 2 * *s - corner_point_s; - *t = 2 * *t - corner_point_t; + // Perform the addition and subtraction using unsigned integers to avoid + // signed integer overflows for bad data. Note that the result will be + // unchanged for non-overflowing cases. + const uint32_t corner_point_s = sign_s * center_value_; + const uint32_t corner_point_t = sign_t * center_value_; + uint32_t us = *s; + uint32_t ut = *t; + us = us + us - corner_point_s; + ut = ut + ut - corner_point_t; if (sign_s * sign_t >= 0) { - int32_t temp = *s; - *s = -*t; - *t = -temp; + uint32_t temp = us; + us = -ut; + ut = -temp; } else { - std::swap(*s, *t); + std::swap(us, ut); } - *s = (*s + corner_point_s) / 2; - *t = (*t + corner_point_t) / 2; + us = us + corner_point_s; + ut = ut + corner_point_t; + + *s = us; + *t = ut; + *s /= 2; + *t /= 2; } void InvertDirection(int32_t *s, int32_t *t) const { @@ -318,7 +330,7 @@ class OctahedronToolBox { // Remaining coordinate can be computed by projecting the (y, z) values onto // the surface of the octahedron. - const float x = 1.f - abs(y) - abs(z); + const float x = 1.f - std::abs(y) - std::abs(z); // |x| is essentially a signed distance from the diagonal edges of the // diamond shown on the figure above. It is positive for all points in the diff --git a/contrib/draco/src/draco/compression/attributes/point_d_vector.h b/contrib/draco/src/draco/compression/attributes/point_d_vector.h index 3b115d500..6ceb454ae 100644 --- a/contrib/draco/src/draco/compression/attributes/point_d_vector.h +++ b/contrib/draco/src/draco/compression/attributes/point_d_vector.h @@ -16,7 +16,9 @@ #ifndef DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_ #define DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_ +#include #include +#include #include #include @@ -99,11 +101,17 @@ class PointDVector { data_(n_items * dimensionality), data0_(data_.data()) {} // random access iterator - class PointDVectorIterator - : public std::iterator { + class PointDVectorIterator { friend class PointDVector; public: + // Iterator traits expected by std libraries. + using iterator_category = std::random_access_iterator_tag; + using value_type = size_t; + using difference_type = size_t; + using pointer = PointDVector *; + using reference = PointDVector &; + // std::iter_swap is called inside of std::partition and needs this // specialized support PseudoPointD operator*() const { diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h index 36c124baa..17899d054 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_decoder.h @@ -22,6 +22,7 @@ #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_decoder.h" #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_parallelogram_shared.h" #include "draco/compression/bit_coders/rans_bit_decoder.h" +#include "draco/core/math_utils.h" #include "draco/core/varint_decoding.h" #include "draco/draco_features.h" @@ -161,7 +162,8 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramDecoder< if (!is_crease) { ++num_used_parallelograms; for (int j = 0; j < num_components; ++j) { - multi_pred_vals[j] += pred_vals[i][j]; + multi_pred_vals[j] = + AddAsUnsigned(multi_pred_vals[j], pred_vals[i][j]); } } } @@ -210,6 +212,9 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramDecoder< if (!DecodeVarint(&num_flags, buffer)) { return false; } + if (num_flags > this->mesh_data().corner_table()->num_corners()) { + return false; + } if (num_flags > 0) { is_crease_edge_[i].resize(num_flags); RAnsBitDecoder decoder; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h index 77df8ee24..736598b15 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_constrained_multi_parallelogram_encoder.h @@ -392,7 +392,7 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramEncoder< RAnsBitEncoder encoder; encoder.StartEncoding(); // Encode the crease edge flags in the reverse vertex order that is needed - // be the decoder. Note that for the currently supported mode, each vertex + // by the decoder. Note that for the currently supported mode, each vertex // has exactly |num_used_parallelograms| edges that need to be encoded. for (int j = static_cast(is_crease_edge_[i].size()) - num_used_parallelograms; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h index fc82e0a8f..9825c7261 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_multi_parallelogram_decoder.h @@ -18,6 +18,7 @@ #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_decoder.h" #include "draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_parallelogram_shared.h" +#include "draco/core/math_utils.h" #include "draco/draco_features.h" namespace draco { @@ -89,7 +90,8 @@ bool MeshPredictionSchemeMultiParallelogramDecoder(data[data_offset + 1])); } - void ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data, + bool ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data, int data_id); private: @@ -123,6 +123,10 @@ bool MeshPredictionSchemeTexCoordsDecoder:: ComputeOriginalValues(const CorrType *in_corr, DataTypeT *out_data, int /* size */, int num_components, const PointIndex *entry_to_point_id_map) { + if (num_components != 2) { + // Corrupt/malformed input. Two output components are req'd. + return false; + } num_components_ = num_components; entry_to_point_id_map_ = entry_to_point_id_map; predicted_value_ = @@ -133,7 +137,9 @@ bool MeshPredictionSchemeTexCoordsDecoder:: static_cast(this->mesh_data().data_to_corner_map()->size()); for (int p = 0; p < corner_map_size; ++p) { const CornerIndex corner_id = this->mesh_data().data_to_corner_map()->at(p); - ComputePredictedValue(corner_id, out_data, p); + if (!ComputePredictedValue(corner_id, out_data, p)) { + return false; + } const int dst_offset = p * num_components; this->transform().ComputeOriginalValue( @@ -159,6 +165,11 @@ bool MeshPredictionSchemeTexCoordsDecoder:: if (num_orientations == 0) { return false; } + if (num_orientations > this->mesh_data().corner_table()->num_corners()) { + // We can't have more orientations than the maximum number of decoded + // values. + return false; + } orientations_.resize(num_orientations); bool last_orientation = true; RAnsBitDecoder decoder; @@ -177,7 +188,7 @@ bool MeshPredictionSchemeTexCoordsDecoder:: } template -void MeshPredictionSchemeTexCoordsDecoder:: +bool MeshPredictionSchemeTexCoordsDecoder:: ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data, int data_id) { // Compute the predicted UV coordinate from the positions on all corners @@ -206,9 +217,17 @@ void MeshPredictionSchemeTexCoordsDecoder:: const Vector2f p_uv = GetTexCoordForEntryId(prev_data_id, data); if (p_uv == n_uv) { // We cannot do a reliable prediction on degenerated UV triangles. - predicted_value_[0] = static_cast(p_uv[0]); - predicted_value_[1] = static_cast(p_uv[1]); - return; + // Technically floats > INT_MAX are undefined, but compilers will + // convert those values to INT_MIN. We are being explicit here for asan. + for (const int i : {0, 1}) { + if (std::isnan(p_uv[i]) || static_cast(p_uv[i]) > INT_MAX || + static_cast(p_uv[i]) < INT_MIN) { + predicted_value_[i] = INT_MIN; + } else { + predicted_value_[i] = static_cast(p_uv[i]); + } + } + return true; } // Get positions at all corners. @@ -282,32 +301,40 @@ void MeshPredictionSchemeTexCoordsDecoder:: const float pnvs = pn_uv[1] * s + n_uv[1]; const float pnvt = pn_uv[1] * t; Vector2f predicted_uv; + if (orientations_.empty()) { + return false; + } // When decoding the data, we already know which orientation to use. const bool orientation = orientations_.back(); orientations_.pop_back(); - if (orientation) + if (orientation) { predicted_uv = Vector2f(pnus - pnvt, pnvs + pnut); - else + } else { predicted_uv = Vector2f(pnus + pnvt, pnvs - pnut); - + } if (std::is_integral::value) { // Round the predicted value for integer types. - if (std::isnan(predicted_uv[0])) { + // Technically floats > INT_MAX are undefined, but compilers will + // convert those values to INT_MIN. We are being explicit here for asan. + const double u = floor(predicted_uv[0] + 0.5); + if (std::isnan(u) || u > INT_MAX || u < INT_MIN) { predicted_value_[0] = INT_MIN; } else { - predicted_value_[0] = static_cast(floor(predicted_uv[0] + 0.5)); + predicted_value_[0] = static_cast(u); } - if (std::isnan(predicted_uv[1])) { + const double v = floor(predicted_uv[1] + 0.5); + if (std::isnan(v) || v > INT_MAX || v < INT_MIN) { predicted_value_[1] = INT_MIN; } else { - predicted_value_[1] = static_cast(floor(predicted_uv[1] + 0.5)); + predicted_value_[1] = static_cast(v); } } else { predicted_value_[0] = static_cast(predicted_uv[0]); predicted_value_[1] = static_cast(predicted_uv[1]); } - return; + + return true; } // Else we don't have available textures on both corners. For such case we // can't use positions for predicting the uv value and we resort to delta @@ -330,12 +357,13 @@ void MeshPredictionSchemeTexCoordsDecoder:: for (int i = 0; i < num_components_; ++i) { predicted_value_[i] = 0; } - return; + return true; } } for (int i = 0; i < num_components_; ++i) { predicted_value_[i] = data[data_offset + i]; } + return true; } } // namespace draco diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h index 741ec66dc..44fcc7a6a 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_encoder.h @@ -98,7 +98,10 @@ bool MeshPredictionSchemeTexCoordsPortableEncoder(this->mesh_data().data_to_corner_map()->size() - 1); p >= 0; --p) { const CornerIndex corner_id = this->mesh_data().data_to_corner_map()->at(p); - predictor_.template ComputePredictedValue(corner_id, in_data, p); + if (!predictor_.template ComputePredictedValue(corner_id, in_data, + p)) { + return false; + } const int dst_offset = p * num_components; this->transform().ComputeCorrection(in_data + dst_offset, diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h index f05e5ddd7..26262fb13 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/mesh_prediction_scheme_tex_coords_portable_predictor.h @@ -17,6 +17,9 @@ #include +#include +#include + #include "draco/attributes/point_attribute.h" #include "draco/core/math_utils.h" #include "draco/core/vector_d.h" @@ -105,10 +108,14 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< next_data_id = mesh_data_.vertex_to_data_map()->at(next_vert_id); prev_data_id = mesh_data_.vertex_to_data_map()->at(prev_vert_id); + typedef VectorD Vec2; + typedef VectorD Vec3; + typedef VectorD Vec2u; + if (prev_data_id < data_id && next_data_id < data_id) { // Both other corners have available UV coordinates for prediction. - const VectorD n_uv = GetTexCoordForEntryId(next_data_id, data); - const VectorD p_uv = GetTexCoordForEntryId(prev_data_id, data); + const Vec2 n_uv = GetTexCoordForEntryId(next_data_id, data); + const Vec2 p_uv = GetTexCoordForEntryId(prev_data_id, data); if (p_uv == n_uv) { // We cannot do a reliable prediction on degenerated UV triangles. predicted_value_[0] = p_uv[0]; @@ -117,9 +124,9 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< } // Get positions at all corners. - const VectorD tip_pos = GetPositionForEntryId(data_id); - const VectorD next_pos = GetPositionForEntryId(next_data_id); - const VectorD prev_pos = GetPositionForEntryId(prev_data_id); + const Vec3 tip_pos = GetPositionForEntryId(data_id); + const Vec3 next_pos = GetPositionForEntryId(next_data_id); + const Vec3 prev_pos = GetPositionForEntryId(prev_data_id); // We use the positions of the above triangle to predict the texture // coordinate on the tip corner C. // To convert the triangle into the UV coordinate system we first compute @@ -135,17 +142,17 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // Where next_pos is point (N), prev_pos is point (P) and tip_pos is the // position of predicted coordinate (C). // - const VectorD pn = prev_pos - next_pos; + const Vec3 pn = prev_pos - next_pos; const uint64_t pn_norm2_squared = pn.SquaredNorm(); if (pn_norm2_squared != 0) { // Compute the projection of C onto PN by computing dot product of CN with // PN and normalizing it by length of PN. This gives us a factor |s| where // |s = PN.Dot(CN) / PN.SquaredNorm2()|. This factor can be used to // compute X in UV space |X_UV| as |X_UV = N_UV + s * PN_UV|. - const VectorD cn = tip_pos - next_pos; + const Vec3 cn = tip_pos - next_pos; const int64_t cn_dot_pn = pn.Dot(cn); - const VectorD pn_uv = p_uv - n_uv; + const Vec2 pn_uv = p_uv - n_uv; // Because we perform all computations with integers, we don't explicitly // compute the normalized factor |s|, but rather we perform all operations // over UV vectors in a non-normalized coordinate system scaled with a @@ -153,19 +160,30 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // // x_uv = X_UV * PN.Norm2Squared() // - const VectorD x_uv = - n_uv * pn_norm2_squared + (cn_dot_pn * pn_uv); - + const int64_t n_uv_absmax_element = + std::max(std::abs(n_uv[0]), std::abs(n_uv[1])); + if (n_uv_absmax_element > + std::numeric_limits::max() / pn_norm2_squared) { + // Return false if the below multiplication would overflow. + return false; + } + const int64_t pn_uv_absmax_element = + std::max(std::abs(pn_uv[0]), std::abs(pn_uv[1])); + if (cn_dot_pn > + std::numeric_limits::max() / pn_uv_absmax_element) { + // Return false if squared length calculation would overflow. + return false; + } + const Vec2 x_uv = n_uv * pn_norm2_squared + (cn_dot_pn * pn_uv); const int64_t pn_absmax_element = std::max(std::max(std::abs(pn[0]), std::abs(pn[1])), std::abs(pn[2])); if (cn_dot_pn > std::numeric_limits::max() / pn_absmax_element) { - // return false if squared length calculation would overflow. + // Return false if squared length calculation would overflow. return false; } // Compute squared length of vector CX in position coordinate system: - const VectorD x_pos = - next_pos + (cn_dot_pn * pn) / pn_norm2_squared; + const Vec3 x_pos = next_pos + (cn_dot_pn * pn) / pn_norm2_squared; const uint64_t cx_norm2_squared = (tip_pos - x_pos).SquaredNorm(); // Compute vector CX_UV in the uv space by rotating vector PN_UV by 90 @@ -182,7 +200,7 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // // cx_uv = CX.Norm2() * PN.Norm2() * Rot(PN_UV) // - VectorD cx_uv(pn_uv[1], -pn_uv[0]); // Rotated PN_UV. + Vec2 cx_uv(pn_uv[1], -pn_uv[0]); // Rotated PN_UV. // Compute CX.Norm2() * PN.Norm2() const uint64_t norm_squared = IntSqrt(cx_norm2_squared * pn_norm2_squared); @@ -191,17 +209,15 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< // Predicted uv coordinate is then computed by either adding or // subtracting CX_UV to/from X_UV. - VectorD predicted_uv; + Vec2 predicted_uv; if (is_encoder_t) { // When encoding, compute both possible vectors and determine which one // results in a better prediction. // Both vectors need to be transformed back from the scaled space to // the real UV coordinate space. - const VectorD predicted_uv_0((x_uv + cx_uv) / - pn_norm2_squared); - const VectorD predicted_uv_1((x_uv - cx_uv) / - pn_norm2_squared); - const VectorD c_uv = GetTexCoordForEntryId(data_id, data); + const Vec2 predicted_uv_0((x_uv + cx_uv) / pn_norm2_squared); + const Vec2 predicted_uv_1((x_uv - cx_uv) / pn_norm2_squared); + const Vec2 c_uv = GetTexCoordForEntryId(data_id, data); if ((c_uv - predicted_uv_0).SquaredNorm() < (c_uv - predicted_uv_1).SquaredNorm()) { predicted_uv = predicted_uv_0; @@ -217,10 +233,12 @@ bool MeshPredictionSchemeTexCoordsPortablePredictor< } const bool orientation = orientations_.back(); orientations_.pop_back(); + // Perform operations in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). if (orientation) { - predicted_uv = (x_uv + cx_uv) / pn_norm2_squared; + predicted_uv = Vec2(Vec2u(x_uv) + Vec2u(cx_uv)) / pn_norm2_squared; } else { - predicted_uv = (x_uv - cx_uv) / pn_norm2_squared; + predicted_uv = Vec2(Vec2u(x_uv) - Vec2u(cx_uv)) / pn_norm2_squared; } } predicted_value_[0] = static_cast(predicted_uv[0]); diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc index f410a6cd2..2338f2f76 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.cc @@ -18,22 +18,58 @@ namespace draco { PredictionSchemeMethod SelectPredictionMethod( int att_id, const PointCloudEncoder *encoder) { - if (encoder->options()->GetSpeed() >= 10) { + return SelectPredictionMethod(att_id, *encoder->options(), encoder); +} + +PredictionSchemeMethod SelectPredictionMethod( + int att_id, const EncoderOptions &options, + const PointCloudEncoder *encoder) { + if (options.GetSpeed() >= 10) { // Selected fastest, though still doing some compression. return PREDICTION_DIFFERENCE; } if (encoder->GetGeometryType() == TRIANGULAR_MESH) { // Use speed setting to select the best encoding method. + const int att_quant = + options.GetAttributeInt(att_id, "quantization_bits", -1); const PointAttribute *const att = encoder->point_cloud()->attribute(att_id); - if (att->attribute_type() == GeometryAttribute::TEX_COORD) { - if (encoder->options()->GetSpeed() < 4) { + if (att_quant != -1 && + att->attribute_type() == GeometryAttribute::TEX_COORD && + att->num_components() == 2) { + // Texture coordinate predictor needs a position attribute that is either + // integer or quantized. For numerical reasons, we require the position + // quantization to be at most 21 bits and the 2*position_quantization + + // uv_quantization < 64 (TODO(b/231259902)). + const PointAttribute *const pos_att = + encoder->point_cloud()->GetNamedAttribute( + GeometryAttribute::POSITION); + bool is_pos_att_valid = false; + if (pos_att) { + if (IsDataTypeIntegral(pos_att->data_type())) { + is_pos_att_valid = true; + } else { + // Check quantization of the position attribute. + const int pos_att_id = encoder->point_cloud()->GetNamedAttributeId( + GeometryAttribute::POSITION); + const int pos_quant = + options.GetAttributeInt(pos_att_id, "quantization_bits", -1); + // Must be quantized but the quantization is restricted to 21 bits and + // 2*|pos_quant|+|att_quant| must be smaller than 64 bits. + if (pos_quant > 0 && pos_quant <= 21 && + 2 * pos_quant + att_quant < 64) { + is_pos_att_valid = true; + } + } + } + + if (is_pos_att_valid && options.GetSpeed() < 4) { // Use texture coordinate prediction for speeds 0, 1, 2, 3. return MESH_PREDICTION_TEX_COORDS_PORTABLE; } } if (att->attribute_type() == GeometryAttribute::NORMAL) { #ifdef DRACO_NORMAL_ENCODING_SUPPORTED - if (encoder->options()->GetSpeed() < 4) { + if (options.GetSpeed() < 4) { // Use geometric normal prediction for speeds 0, 1, 2, 3. // For this prediction, the position attribute needs to be either // integer or quantized as well. @@ -43,8 +79,8 @@ PredictionSchemeMethod SelectPredictionMethod( encoder->point_cloud()->GetNamedAttribute( GeometryAttribute::POSITION); if (pos_att && (IsDataTypeIntegral(pos_att->data_type()) || - encoder->options()->GetAttributeInt( - pos_att_id, "quantization_bits", -1) > 0)) { + options.GetAttributeInt(pos_att_id, "quantization_bits", + -1) > 0)) { return MESH_PREDICTION_GEOMETRIC_NORMAL; } } @@ -52,11 +88,10 @@ PredictionSchemeMethod SelectPredictionMethod( return PREDICTION_DIFFERENCE; // default } // Handle other attribute types. - if (encoder->options()->GetSpeed() >= 8) { + if (options.GetSpeed() >= 8) { return PREDICTION_DIFFERENCE; } - if (encoder->options()->GetSpeed() >= 2 || - encoder->point_cloud()->num_points() < 40) { + if (options.GetSpeed() >= 2 || encoder->point_cloud()->num_points() < 40) { // Parallelogram prediction is used for speeds 2 - 7 or when the overhead // of using constrained multi-parallelogram would be too high. return MESH_PREDICTION_PARALLELOGRAM; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h index 40a7683aa..11db5a62e 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h @@ -38,6 +38,10 @@ namespace draco { PredictionSchemeMethod SelectPredictionMethod(int att_id, const PointCloudEncoder *encoder); +PredictionSchemeMethod SelectPredictionMethod(int att_id, + const EncoderOptions &options, + const PointCloudEncoder *encoder); + // Factory class for creating mesh prediction schemes. template struct MeshPredictionSchemeEncoderFactory { @@ -97,10 +101,11 @@ CreatePredictionSchemeForEncoder(PredictionSchemeMethod method, int att_id, // template nature of the prediction schemes). const MeshEncoder *const mesh_encoder = static_cast(encoder); + const uint16_t bitstream_version = kDracoMeshBitstreamVersion; auto ret = CreateMeshPredictionScheme< MeshEncoder, PredictionSchemeEncoder, MeshPredictionSchemeEncoderFactory>( - mesh_encoder, method, att_id, transform, kDracoMeshBitstreamVersion); + mesh_encoder, method, att_id, transform, bitstream_version); if (ret) { return ret; } diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h index 5a6c7c2dd..e9e345343 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_decoding_transform.h @@ -21,6 +21,7 @@ #include "draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_base.h" #include "draco/core/decoder_buffer.h" #include "draco/core/macros.h" +#include "draco/core/math_utils.h" #include "draco/core/vector_d.h" namespace draco { @@ -98,9 +99,8 @@ class PredictionSchemeNormalOctahedronCanonicalizedDecodingTransform if (!pred_is_in_bottom_left) { pred = this->RotatePoint(pred, rotation_count); } - Point2 orig = pred + corr; - orig[0] = this->ModMax(orig[0]); - orig[1] = this->ModMax(orig[1]); + Point2 orig(this->ModMax(AddAsUnsigned(pred[0], corr[0])), + this->ModMax(AddAsUnsigned(pred[1], corr[1]))); if (!pred_is_in_bottom_left) { const int32_t reverse_rotation_count = (4 - rotation_count) % 4; orig = this->RotatePoint(orig, reverse_rotation_count); diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc index 8c8932f77..298758d8c 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc @@ -25,10 +25,10 @@ class PredictionSchemeNormalOctahedronCanonicalizedTransformTest Transform; typedef Transform::Point2 Point2; - void TestComputeCorrection(const Transform &transform, const int32_t &ox, - const int32_t &oy, const int32_t &px, - const int32_t &py, const int32_t &cx, - const int32_t &cy) { + void TestComputeCorrection(const Transform &transform, const int32_t ox, + const int32_t oy, const int32_t px, + const int32_t py, const int32_t cx, + const int32_t cy) { const int32_t o[2] = {ox + 7, oy + 7}; const int32_t p[2] = {px + 7, py + 7}; int32_t corr[2] = {500, 500}; @@ -38,7 +38,7 @@ class PredictionSchemeNormalOctahedronCanonicalizedTransformTest } void TestGetRotationCount(const Transform &transform, const Point2 &pred, - const int32_t &rot_dir) { + const int32_t rot_dir) { const int32_t rotation_count = transform.GetRotationCount(pred); ASSERT_EQ(rot_dir, rotation_count); } diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h index a1bc4a327..d3705c8ad 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_decoding_transform.h @@ -80,19 +80,31 @@ class PredictionSchemeNormalOctahedronDecodingTransform private: Point2 ComputeOriginalValue(Point2 pred, const Point2 &corr) const { const Point2 t(this->center_value(), this->center_value()); - pred = pred - t; + typedef typename std::make_unsigned::type UnsignedDataTypeT; + typedef VectorD Point2u; + + // Perform the addition in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). + pred = Point2(Point2u(pred) - Point2u(t)); const bool pred_is_in_diamond = this->IsInDiamond(pred[0], pred[1]); if (!pred_is_in_diamond) { this->InvertDiamond(&pred[0], &pred[1]); } - Point2 orig = pred + corr; + + // Perform the addition in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). + Point2 orig(Point2u(pred) + Point2u(corr)); + orig[0] = this->ModMax(orig[0]); orig[1] = this->ModMax(orig[1]); if (!pred_is_in_diamond) { this->InvertDiamond(&orig[0], &orig[1]); } - orig = orig + t; + + // Perform the addition in unsigned type to avoid signed integer overflow. + // Note that the result will be the same (for non-overflowing values). + orig = Point2(Point2u(orig) + Point2u(t)); return orig; } }; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc index 1001b19fa..1403973c4 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_transform_test.cc @@ -23,10 +23,10 @@ class PredictionSchemeNormalOctahedronTransformTest : public ::testing::Test { Transform; typedef Transform::Point2 Point2; - void TestComputeCorrection(const Transform &transform, const int32_t &ox, - const int32_t &oy, const int32_t &px, - const int32_t &py, const int32_t &cx, - const int32_t &cy) { + void TestComputeCorrection(const Transform &transform, const int32_t ox, + const int32_t oy, const int32_t px, + const int32_t py, const int32_t cx, + const int32_t cy) { const int32_t o[2] = {ox + 7, oy + 7}; const int32_t p[2] = {px + 7, py + 7}; int32_t corr[2] = {500, 500}; diff --git a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h index 26f61fbaf..bba3de09c 100644 --- a/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h +++ b/contrib/draco/src/draco/compression/attributes/prediction_schemes/prediction_scheme_wrap_transform_base.h @@ -70,10 +70,10 @@ class PredictionSchemeWrapTransformBase { clamped_value_[i] = predicted_val[i]; } } - return &clamped_value_[0]; + return clamped_value_.data(); } - // TODO(hemmer): Consider refactoring to avoid this dummy. + // TODO(b/199760123): Consider refactoring to avoid this dummy. int quantization_bits() const { DRACO_DCHECK(false); return -1; diff --git a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc index 83f42125a..17f32fc16 100644 --- a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc +++ b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_decoder.cc @@ -148,8 +148,9 @@ bool SequentialIntegerAttributeDecoder::DecodeIntegerValues( return false; } for (size_t i = 0; i < num_values; ++i) { - if (!in_buffer->Decode(portable_attribute_data + i, num_bytes)) + if (!in_buffer->Decode(portable_attribute_data + i, num_bytes)) { return false; + } } } } @@ -228,12 +229,13 @@ void SequentialIntegerAttributeDecoder::StoreTypedValues(uint32_t num_values) { void SequentialIntegerAttributeDecoder::PreparePortableAttribute( int num_entries, int num_components) { - GeometryAttribute va; - va.Init(attribute()->attribute_type(), nullptr, num_components, DT_INT32, + GeometryAttribute ga; + ga.Init(attribute()->attribute_type(), nullptr, num_components, DT_INT32, false, num_components * DataTypeLength(DT_INT32), 0); - std::unique_ptr port_att(new PointAttribute(va)); + std::unique_ptr port_att(new PointAttribute(ga)); port_att->SetIdentityMapping(); port_att->Reset(num_entries); + port_att->set_unique_id(attribute()->unique_id()); SetPortableAttribute(std::move(port_att)); } diff --git a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc index e66a0a8a4..5f673be42 100644 --- a/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc +++ b/contrib/draco/src/draco/compression/attributes/sequential_integer_attribute_encoder.cc @@ -138,9 +138,11 @@ bool SequentialIntegerAttributeEncoder::EncodeValues( // All integer values are initialized. Process them using the prediction // scheme if we have one. if (prediction_scheme_) { - prediction_scheme_->ComputeCorrectionValues( - portable_attribute_data, &encoded_data[0], num_values, num_components, - point_ids.data()); + if (!prediction_scheme_->ComputeCorrectionValues( + portable_attribute_data, &encoded_data[0], num_values, + num_components, point_ids.data())) { + return false; + } } if (prediction_scheme_ == nullptr || diff --git a/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc b/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc index 2e20e89e6..3c5ef0ebc 100644 --- a/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc +++ b/contrib/draco/src/draco/compression/attributes/sequential_normal_attribute_encoder.cc @@ -20,8 +20,9 @@ namespace draco { bool SequentialNormalAttributeEncoder::Init(PointCloudEncoder *encoder, int attribute_id) { - if (!SequentialIntegerAttributeEncoder::Init(encoder, attribute_id)) + if (!SequentialIntegerAttributeEncoder::Init(encoder, attribute_id)) { return false; + } // Currently this encoder works only for 3-component normal vectors. if (attribute()->num_components() != 3) { return false; diff --git a/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h b/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h index b9fbc2d6f..6273692a2 100644 --- a/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h +++ b/contrib/draco/src/draco/compression/bit_coders/direct_bit_decoder.h @@ -47,14 +47,13 @@ class DirectBitDecoder { // Decode the next |nbits| and return the sequence in |value|. |nbits| must be // > 0 and <= 32. - void DecodeLeastSignificantBits32(int nbits, uint32_t *value) { + bool DecodeLeastSignificantBits32(int nbits, uint32_t *value) { DRACO_DCHECK_EQ(true, nbits <= 32); DRACO_DCHECK_EQ(true, nbits > 0); const int remaining = 32 - num_used_bits_; if (nbits <= remaining) { if (pos_ == bits_.end()) { - *value = 0; - return; + return false; } *value = (*pos_ << num_used_bits_) >> (32 - nbits); num_used_bits_ += nbits; @@ -64,8 +63,7 @@ class DirectBitDecoder { } } else { if (pos_ + 1 == bits_.end()) { - *value = 0; - return; + return false; } const uint32_t value_l = ((*pos_) << num_used_bits_); num_used_bits_ = nbits - remaining; @@ -73,6 +71,7 @@ class DirectBitDecoder { const uint32_t value_r = (*pos_) >> (32 - num_used_bits_); *value = (value_l >> (32 - num_used_bits_ - remaining)) | value_r; } + return true; } void EndDecoding() {} diff --git a/contrib/draco/src/draco/compression/config/encoder_options.h b/contrib/draco/src/draco/compression/config/encoder_options.h index ed1b02068..e8a55bbba 100644 --- a/contrib/draco/src/draco/compression/config/encoder_options.h +++ b/contrib/draco/src/draco/compression/config/encoder_options.h @@ -65,6 +65,10 @@ class EncoderOptionsBase : public DracoOptions { this->SetGlobalInt("encoding_speed", encoding_speed); this->SetGlobalInt("decoding_speed", decoding_speed); } + bool IsSpeedSet() const { + return this->IsGlobalOptionSet("encoding_speed") || + this->IsGlobalOptionSet("decoding_speed"); + } // Sets a given feature as supported or unsupported by the target decoder. // Encoder will always use only supported features when encoding the input diff --git a/contrib/draco/src/draco/compression/decode_test.cc b/contrib/draco/src/draco/compression/decode_test.cc index 198714690..8f3e7f4e9 100644 --- a/contrib/draco/src/draco/compression/decode_test.cc +++ b/contrib/draco/src/draco/compression/decode_test.cc @@ -17,9 +17,11 @@ #include #include +#include "draco/compression/encode.h" #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/io/file_utils.h" +#include "draco/io/obj_encoder.h" namespace { @@ -166,4 +168,78 @@ TEST_F(DecodeTest, TestSkipAttributeTransformWithNoQuantization) { ASSERT_EQ(pos_att->GetAttributeTransformData(), nullptr); } +TEST_F(DecodeTest, TestSkipAttributeTransformUniqueId) { + // Tests that decoders preserve unique id of attributes even when their + // attribute transforms are skipped. + const std::string file_name = "cube_att.obj"; + auto src_mesh = draco::ReadMeshFromTestFile(file_name); + ASSERT_NE(src_mesh, nullptr); + + constexpr int kPosUniqueId = 7; + constexpr int kNormUniqueId = 42; + // Set unique ids for some of the attributes. + src_mesh + ->attribute( + src_mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION)) + ->set_unique_id(kPosUniqueId); + src_mesh + ->attribute( + src_mesh->GetNamedAttributeId(draco::GeometryAttribute::NORMAL)) + ->set_unique_id(kNormUniqueId); + + draco::EncoderBuffer encoder_buffer; + draco::Encoder encoder; + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 10); + encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 11); + encoder.EncodeMeshToBuffer(*src_mesh, &encoder_buffer); + + // Create a draco decoding buffer. + draco::DecoderBuffer buffer; + buffer.Init(encoder_buffer.data(), encoder_buffer.size()); + + // First we decode the mesh without skipping the attribute transforms. + draco::Decoder decoder_no_skip; + std::unique_ptr mesh_no_skip = + decoder_no_skip.DecodeMeshFromBuffer(&buffer).value(); + ASSERT_NE(mesh_no_skip, nullptr); + + // Now we decode it again while skipping some attributes. + draco::Decoder decoder_skip; + // Make sure we skip dequantization for the position and normal attribute. + decoder_skip.SetSkipAttributeTransform(draco::GeometryAttribute::POSITION); + decoder_skip.SetSkipAttributeTransform(draco::GeometryAttribute::NORMAL); + + // Decode the input data into a geometry. + buffer.Init(encoder_buffer.data(), encoder_buffer.size()); + std::unique_ptr mesh_skip = + decoder_skip.DecodeMeshFromBuffer(&buffer).value(); + ASSERT_NE(mesh_skip, nullptr); + + // Compare the unique ids. + const draco::PointAttribute *const pos_att_no_skip = + mesh_no_skip->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att_no_skip, nullptr); + ASSERT_EQ(pos_att_no_skip->data_type(), draco::DataType::DT_FLOAT32); + + const draco::PointAttribute *const pos_att_skip = + mesh_skip->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att_skip, nullptr); + ASSERT_EQ(pos_att_skip->data_type(), draco::DataType::DT_INT32); + + const draco::PointAttribute *const norm_att_no_skip = + mesh_no_skip->GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_NE(norm_att_no_skip, nullptr); + ASSERT_EQ(norm_att_no_skip->data_type(), draco::DataType::DT_FLOAT32); + + const draco::PointAttribute *const norm_att_skip = + mesh_skip->GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_NE(norm_att_skip, nullptr); + ASSERT_EQ(norm_att_skip->data_type(), draco::DataType::DT_INT32); + + ASSERT_EQ(pos_att_skip->unique_id(), pos_att_no_skip->unique_id()); + ASSERT_EQ(norm_att_skip->unique_id(), norm_att_no_skip->unique_id()); + std::cout << pos_att_skip->unique_id() << " " << norm_att_skip->unique_id() + << std::endl; +} + } // namespace diff --git a/contrib/draco/src/draco/compression/draco_compression_options.cc b/contrib/draco/src/draco/compression/draco_compression_options.cc new file mode 100644 index 000000000..08171c678 --- /dev/null +++ b/contrib/draco/src/draco/compression/draco_compression_options.cc @@ -0,0 +1,59 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/compression/draco_compression_options.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +SpatialQuantizationOptions::SpatialQuantizationOptions(int quantization_bits) { + SetQuantizationBits(quantization_bits); +} + +void SpatialQuantizationOptions::SetQuantizationBits(int quantization_bits) { + mode_ = LOCAL_QUANTIZATION_BITS; + quantization_bits_ = quantization_bits; +} + +bool SpatialQuantizationOptions::AreQuantizationBitsDefined() const { + return mode_ == LOCAL_QUANTIZATION_BITS; +} + +SpatialQuantizationOptions &SpatialQuantizationOptions::SetGrid(float spacing) { + mode_ = GLOBAL_GRID; + spacing_ = spacing; + return *this; +} + +bool SpatialQuantizationOptions::operator==( + const SpatialQuantizationOptions &other) const { + if (mode_ != other.mode_) { + return false; + } + if (mode_ == LOCAL_QUANTIZATION_BITS) { + if (quantization_bits_ != other.quantization_bits_) { + return false; + } + } else if (mode_ == GLOBAL_GRID) { + if (spacing_ != other.spacing_) { + return false; + } + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/compression/draco_compression_options.h b/contrib/draco/src/draco/compression/draco_compression_options.h new file mode 100644 index 000000000..31a4418ed --- /dev/null +++ b/contrib/draco/src/draco/compression/draco_compression_options.h @@ -0,0 +1,141 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_COMPRESSION_DRACO_COMPRESSION_OPTIONS_H_ +#define DRACO_COMPRESSION_DRACO_COMPRESSION_OPTIONS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" + +namespace draco { + +// Quantization options for positions. Currently there are two modes for +// quantizing positions: +// +// 1. Quantization bits: +// - User defined number of quantization bits that is evenly distributed +// to cover the compressed geometry. +// 2. Grid: +// - Positions are snapped to a global grid defined by grid spacing. +// - This method is primarily intended to be used when the location of +// quantized vertices needs to be consistent between multiple +// geometries. +class SpatialQuantizationOptions { + public: + explicit SpatialQuantizationOptions(int quantization_bits); + + // Sets quantization bits that are going to be used for the compressed + // geometry. If the geometry is a scene, the same number of quantization bits + // is going to be applied to each mesh of the scene. Quantized values are + // going to be distributed within the bounds of individual meshes. + void SetQuantizationBits(int quantization_bits); + + // If this returns true, quantization_bits() should be used to get the + // desired number of quantization bits for compression. Otherwise the grid + // mode is selected and spacing() should be used to get the desired grid + // spacing. + bool AreQuantizationBitsDefined() const; + const int quantization_bits() const { return quantization_bits_; } + + // Defines quantization grid used for the compressed geometry. All vertices + // are going to be snapped to the nearest grid vertex that corresponds to an + // integer quantized position. |spacing| defines the distance between two grid + // vertices. E.g. a grid with |spacing| = 10 would have grid vertices at + // locations {10 * i, 10 * j, 10 * k} where i, j, k are integer numbers. + SpatialQuantizationOptions &SetGrid(float spacing); + + const float spacing() const { return spacing_; } + + bool operator==(const SpatialQuantizationOptions &other) const; + + private: + enum Mode { LOCAL_QUANTIZATION_BITS, GLOBAL_GRID }; + Mode mode_ = LOCAL_QUANTIZATION_BITS; + int quantization_bits_; // Default quantization bits for positions. + float spacing_ = 0.f; +}; + +// TODO(fgalligan): Add support for unified_position_quantization. +// Struct to hold Draco compression options. +struct DracoCompressionOptions { + int compression_level = 7; // compression level [0-10], most=10, least=0. + SpatialQuantizationOptions quantization_position{11}; + int quantization_bits_normal = 8; + int quantization_bits_tex_coord = 10; + int quantization_bits_color = 8; + int quantization_bits_generic = 8; + int quantization_bits_tangent = 8; + int quantization_bits_weight = 8; + bool find_non_degenerate_texture_quantization = false; + + bool operator==(const DracoCompressionOptions &other) const { + return compression_level == other.compression_level && + quantization_position == other.quantization_position && + quantization_bits_normal == other.quantization_bits_normal && + quantization_bits_tex_coord == other.quantization_bits_tex_coord && + quantization_bits_color == other.quantization_bits_color && + quantization_bits_generic == other.quantization_bits_generic && + quantization_bits_tangent == other.quantization_bits_tangent && + quantization_bits_weight == other.quantization_bits_weight && + find_non_degenerate_texture_quantization == + other.find_non_degenerate_texture_quantization; + } + + bool operator!=(const DracoCompressionOptions &other) const { + return !(*this == other); + } + + Status Check() const { + DRACO_RETURN_IF_ERROR( + Validate("Compression level", compression_level, 0, 10)); + if (quantization_position.AreQuantizationBitsDefined()) { + DRACO_RETURN_IF_ERROR(Validate("Position quantization", + quantization_position.quantization_bits(), + 0, 30)); + } else { + if (quantization_position.spacing() <= 0.f) { + return ErrorStatus("Position quantization spacing is invalid."); + } + } + DRACO_RETURN_IF_ERROR( + Validate("Normals quantization", quantization_bits_normal, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Tex coord quantization", quantization_bits_tex_coord, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Color quantization", quantization_bits_color, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Generic quantization", quantization_bits_generic, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Tangent quantization", quantization_bits_tangent, 0, 30)); + DRACO_RETURN_IF_ERROR( + Validate("Weights quantization", quantization_bits_weight, 0, 30)); + return OkStatus(); + } + + static Status Validate(const std::string &name, int value, int min, int max) { + if (value < min || value > max) { + const std::string range = + "[" + std::to_string(min) + "-" + std::to_string(max) + "]."; + return Status(Status::DRACO_ERROR, name + " is out of range " + range); + } + return OkStatus(); + } +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_COMPRESSION_DRACO_COMPRESSION_OPTIONS_H_ diff --git a/contrib/draco/src/draco/compression/draco_compression_options_test.cc b/contrib/draco/src/draco/compression/draco_compression_options_test.cc new file mode 100644 index 000000000..415295211 --- /dev/null +++ b/contrib/draco/src/draco/compression/draco_compression_options_test.cc @@ -0,0 +1,45 @@ +#include "draco/compression/draco_compression_options.h" + +#include "draco/core/draco_test_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace { + +TEST(DracoCompressionOptionsTest, TestPositionQuantizationBits) { + // Test verifies that we can define draco compression options using + // quantization bits. + draco::SpatialQuantizationOptions options(10); + + // Quantization bits should be used by default. + ASSERT_TRUE(options.AreQuantizationBitsDefined()); + ASSERT_EQ(options.quantization_bits(), 10); + + // Change the quantization bits. + options.SetQuantizationBits(9); + ASSERT_TRUE(options.AreQuantizationBitsDefined()); + ASSERT_EQ(options.quantization_bits(), 9); + + // If we select the grid, quantization bits should not be used. + options.SetGrid(0.5f); + ASSERT_FALSE(options.AreQuantizationBitsDefined()); +} + +TEST(DracoCompressionOptionsTest, TestPositionQuantizationGrid) { + // Test verifies that we can define draco compression options using + // quantization grid. + draco::SpatialQuantizationOptions options(10); + + // Quantization bits should be used by default. + ASSERT_TRUE(options.AreQuantizationBitsDefined()); + + // Set the grid parameters. + options.SetGrid(0.25f); + ASSERT_FALSE(options.AreQuantizationBitsDefined()); + + ASSERT_EQ(options.spacing(), 0.25f); +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/compression/encode.h b/contrib/draco/src/draco/compression/encode.h index bce8b34c2..00ccb9b2e 100644 --- a/contrib/draco/src/draco/compression/encode.h +++ b/contrib/draco/src/draco/compression/encode.h @@ -129,7 +129,6 @@ class Encoder // call of EncodePointCloudToBuffer or EncodeMeshToBuffer is going to fail. void SetEncodingMethod(int encoding_method); - protected: // Creates encoder options for the expert encoder used during the actual // encoding. EncoderOptions CreateExpertEncoderOptions(const PointCloud &pc) const; diff --git a/contrib/draco/src/draco/compression/encode_base.h b/contrib/draco/src/draco/compression/encode_base.h index c501bc4fa..6211efc22 100644 --- a/contrib/draco/src/draco/compression/encode_base.h +++ b/contrib/draco/src/draco/compression/encode_base.h @@ -98,7 +98,7 @@ class EncoderBase { "Invalid prediction scheme for attribute type."); } } - // TODO(hemmer): Try to enable more prediction schemes for normals. + // TODO(b/199760123): Try to enable more prediction schemes for normals. if (att_type == GeometryAttribute::NORMAL) { if (!(prediction_scheme == PREDICTION_DIFFERENCE || prediction_scheme == MESH_PREDICTION_GEOMETRIC_NORMAL)) { diff --git a/contrib/draco/src/draco/compression/encode_test.cc b/contrib/draco/src/draco/compression/encode_test.cc index fde4f6f5b..00d834703 100644 --- a/contrib/draco/src/draco/compression/encode_test.cc +++ b/contrib/draco/src/draco/compression/encode_test.cc @@ -26,6 +26,7 @@ #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/core/vector_d.h" +#include "draco/io/file_utils.h" #include "draco/io/obj_decoder.h" #include "draco/mesh/triangle_soup_mesh_builder.h" #include "draco/point_cloud/point_cloud_builder.h" @@ -213,16 +214,14 @@ class EncodeTest : public ::testing::Test { draco::Decoder decoder; if (mesh) { - auto maybe_mesh = decoder.DecodeMeshFromBuffer(&decoder_buffer); - ASSERT_TRUE(maybe_mesh.ok()); - auto decoded_mesh = std::move(maybe_mesh).value(); + DRACO_ASSIGN_OR_ASSERT(auto decoded_mesh, + decoder.DecodeMeshFromBuffer(&decoder_buffer)); ASSERT_NE(decoded_mesh, nullptr); ASSERT_EQ(decoded_mesh->num_points(), encoder.num_encoded_points()); ASSERT_EQ(decoded_mesh->num_faces(), encoder.num_encoded_faces()); } else { - auto maybe_pc = decoder.DecodePointCloudFromBuffer(&decoder_buffer); - ASSERT_TRUE(maybe_pc.ok()); - auto decoded_pc = std::move(maybe_pc).value(); + DRACO_ASSIGN_OR_ASSERT( + auto decoded_pc, decoder.DecodePointCloudFromBuffer(&decoder_buffer)); ASSERT_EQ(decoded_pc->num_points(), encoder.num_encoded_points()); } } @@ -274,7 +273,7 @@ TEST_F(EncodeTest, TestLinesObj) { encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 16); draco::EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestQuantizedInfinity) { @@ -315,7 +314,7 @@ TEST_F(EncodeTest, TestUnquantizedInfinity) { encoder.SetEncodingMethod(draco::POINT_CLOUD_SEQUENTIAL_ENCODING); draco::EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestQuantizedAndUnquantizedAttributes) { @@ -330,7 +329,7 @@ TEST_F(EncodeTest, TestQuantizedAndUnquantizedAttributes) { encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 11); encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 0); draco::EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestKdTreeEncoding) { @@ -348,7 +347,7 @@ TEST_F(EncodeTest, TestKdTreeEncoding) { // Now set quantization for the position attribute which should make // the encoder happy. encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 16); - ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodePointCloudToBuffer(*pc, &buffer)); } TEST_F(EncodeTest, TestTrackingOfNumberOfEncodedEntries) { @@ -373,7 +372,7 @@ TEST_F(EncodeTest, TestTrackingOfNumberOfEncodedEntriesNotSet) { draco::EncoderBuffer buffer; draco::Encoder encoder; - ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer)); ASSERT_EQ(encoder.num_encoded_points(), 0); ASSERT_EQ(encoder.num_encoded_faces(), 0); } @@ -404,4 +403,170 @@ TEST_F(EncodeTest, TestNoPosQuantizationNormalCoding) { ASSERT_NE(decoded_mesh, nullptr); } +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST_F(EncodeTest, TestDracoCompressionOptions) { + // This test verifies that we can set the encoder's compression options via + // draco::Mesh's compression options. + const auto mesh = draco::ReadMeshFromTestFile("test_nm.obj"); + ASSERT_NE(mesh, nullptr); + + // First set compression level and quantization manually. + draco::Encoder encoder_manual; + draco::EncoderBuffer buffer_manual; + encoder_manual.SetAttributeQuantization(draco::GeometryAttribute::POSITION, + 8); + encoder_manual.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 7); + encoder_manual.SetSpeedOptions(4, 4); + + DRACO_ASSERT_OK(encoder_manual.EncodeMeshToBuffer(*mesh, &buffer_manual)); + + // Now do the same with options provided via DracoCompressionOptions. + draco::DracoCompressionOptions compression_options; + compression_options.compression_level = 6; + compression_options.quantization_position.SetQuantizationBits(8); + compression_options.quantization_bits_normal = 7; + mesh->SetCompressionOptions(compression_options); + mesh->SetCompressionEnabled(true); + + draco::Encoder encoder_auto; + draco::EncoderBuffer buffer_auto; + DRACO_ASSERT_OK(encoder_auto.EncodeMeshToBuffer(*mesh, &buffer_auto)); + + // Ensure that both encoders produce the same result. + ASSERT_EQ(buffer_manual.size(), buffer_auto.size()); + + // Now change some of the mesh's compression settings and ensure the + // compression changes as well. + compression_options.compression_level = 7; + mesh->SetCompressionOptions(compression_options); + buffer_auto.Clear(); + DRACO_ASSERT_OK(encoder_auto.EncodeMeshToBuffer(*mesh, &buffer_auto)); + ASSERT_NE(buffer_manual.size(), buffer_auto.size()); + + // Check that |mesh| compression options do not override the encoder options. + mesh->GetCompressionOptions().compression_level = 10; + mesh->GetCompressionOptions().quantization_position.SetQuantizationBits(10); + mesh->GetCompressionOptions().quantization_bits_normal = 10; + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder_manual.EncodeMeshToBuffer(*mesh, &buffer)); + ASSERT_EQ(buffer.size(), buffer_manual.size()); +} + +TEST_F(EncodeTest, TestDracoCompressionOptionsManualOverride) { + // This test verifies that we can use encoder's option to override compression + // options provided in draco::Mesh's compression options. + const auto mesh = draco::ReadMeshFromTestFile("test_nm.obj"); + ASSERT_NE(mesh, nullptr); + + // Set some compression options. + draco::DracoCompressionOptions compression_options; + compression_options.compression_level = 6; + compression_options.quantization_position.SetQuantizationBits(8); + compression_options.quantization_bits_normal = 7; + mesh->SetCompressionOptions(compression_options); + mesh->SetCompressionEnabled(true); + + draco::Encoder encoder; + draco::EncoderBuffer buffer_no_override; + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer_no_override)); + + // Now override some options and ensure the compression is different. + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 5); + draco::EncoderBuffer buffer_with_override; + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer_with_override)); + ASSERT_LT(buffer_with_override.size(), buffer_no_override.size()); +} + +TEST_F(EncodeTest, TestDracoCompressionOptionsGridQuantization) { + // Test verifies that we can set position quantization via grid spacing. + + // 1x1x1 cube. + const auto mesh = draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + mesh->SetCompressionEnabled(true); + + // Set grid quantization for positions. + draco::DracoCompressionOptions compression_options; + // This should result in 10x10x10 quantization. + compression_options.quantization_position.SetGrid(0.1); + mesh->SetCompressionOptions(compression_options); + + draco::ExpertEncoder encoder(*mesh); + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeToBuffer(&buffer)); + + // The grid options should be reflected in the |encoder|. Check that the + // computed values are correct. + const int pos_att_id = + mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION); + draco::Vector3f origin; + encoder.options().GetAttributeVector(pos_att_id, "quantization_origin", 3, + &origin[0]); + ASSERT_EQ(origin, draco::Vector3f(0.f, 0.f, 0.f)); + + // We need 4 quantization bits (for 10 values). + ASSERT_EQ( + encoder.options().GetAttributeInt(pos_att_id, "quantization_bits", -1), + 4); + + // The quantization range should be ((1 << quantization_bits) - 1) * spacing. + ASSERT_NEAR(encoder.options().GetAttributeFloat(pos_att_id, + "quantization_range", 0.f), + 15.f * 0.1f, 1e-6f); +} + +TEST_F(EncodeTest, TestDracoCompressionOptionsGridQuantizationWithOffset) { + // Test verifies that we can set position quantization via grid spacing when + // the geometry is not perfectly aligned with the quantization grid. + + // 1x1x1 cube. + const auto mesh = draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Move all positions a bit. + auto *pos_att = mesh->attribute( + mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION)); + for (draco::AttributeValueIndex avi(0); avi < pos_att->size(); ++avi) { + draco::Vector3f pos; + pos_att->GetValue(avi, &pos[0]); + pos = pos + draco::Vector3f(-0.55f, 0.65f, 10.75f); + pos_att->SetAttributeValue(avi, &pos[0]); + } + + mesh->SetCompressionEnabled(true); + + // Set grid quantization for positions. + draco::DracoCompressionOptions compression_options; + // This should result in 16x16x16 quantization if the grid was perfectly + // aligned but since it is not we should expect 17 or 18 values per component. + compression_options.quantization_position.SetGrid(0.0625f); + mesh->SetCompressionOptions(compression_options); + + draco::ExpertEncoder encoder(*mesh); + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeToBuffer(&buffer)); + + // The grid options should be reflected in the |encoder|. Check that the + // computed values are correct. + const int pos_att_id = + mesh->GetNamedAttributeId(draco::GeometryAttribute::POSITION); + draco::Vector3f origin; + encoder.options().GetAttributeVector(pos_att_id, "quantization_origin", 3, + &origin[0]); + // The origin is the first lower value on the quantization grid for each + // component of the mesh. + ASSERT_EQ(origin, draco::Vector3f(-0.5625f, 0.625f, 10.75f)); + + // We need 5 quantization bits (for 17-18 values). + ASSERT_EQ( + encoder.options().GetAttributeInt(pos_att_id, "quantization_bits", -1), + 5); + + // The quantization range should be ((1 << quantization_bits) - 1) * spacing. + ASSERT_NEAR(encoder.options().GetAttributeFloat(pos_att_id, + "quantization_range", 0.f), + 31.f * 0.0625f, 1e-6f); +} +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace diff --git a/contrib/draco/src/draco/compression/entropy/ans.h b/contrib/draco/src/draco/compression/entropy/ans.h index c71d58975..313546fee 100644 --- a/contrib/draco/src/draco/compression/entropy/ans.h +++ b/contrib/draco/src/draco/compression/entropy/ans.h @@ -391,7 +391,6 @@ class RAnsEncoder { ans_.buf[ans_.buf_offset++] = ans_.state % DRACO_ANS_IO_BASE; ans_.state /= DRACO_ANS_IO_BASE; } - // TODO(ostava): The division and multiplication should be optimized. ans_.state = (ans_.state / p) * rans_precision + ans_.state % p + sym->cum_prob; } diff --git a/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h b/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h index 10cdc6781..3b408c079 100644 --- a/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h +++ b/contrib/draco/src/draco/compression/entropy/rans_symbol_decoder.h @@ -75,6 +75,13 @@ bool RAnsSymbolDecoder::Create( return false; } } + // Check that decoded number of symbols is not unreasonably high. Remaining + // buffer size must be at least |num_symbols| / 64 bytes to contain the + // probability table. The |prob_data| below is one byte but it can be + // theoretically stored for each 64th symbol. + if (num_symbols_ / 64 > buffer->remaining_size()) { + return false; + } probability_table_.resize(num_symbols_); if (num_symbols_ == 0) { return true; diff --git a/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h b/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h index 4e07ec871..4b738b50a 100644 --- a/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h +++ b/contrib/draco/src/draco/compression/entropy/rans_symbol_encoder.h @@ -125,8 +125,8 @@ bool RAnsSymbolEncoder::Create( for (int i = 0; i < num_symbols; ++i) { sorted_probabilities[i] = i; } - std::sort(sorted_probabilities.begin(), sorted_probabilities.end(), - ProbabilityLess(&probability_table_)); + std::stable_sort(sorted_probabilities.begin(), sorted_probabilities.end(), + ProbabilityLess(&probability_table_)); if (total_rans_prob < rans_precision_) { // This happens rather infrequently, just add the extra needed precision // to the most frequent symbol. diff --git a/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc b/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc index 93d29971c..79e811818 100644 --- a/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc +++ b/contrib/draco/src/draco/compression/entropy/symbol_decoding.cc @@ -72,7 +72,7 @@ bool DecodeTaggedSymbols(uint32_t num_values, int num_components, int value_id = 0; for (uint32_t i = 0; i < num_values; i += num_components) { // Decode the tag. - const int bit_length = tag_decoder.DecodeSymbol(); + const uint32_t bit_length = tag_decoder.DecodeSymbol(); // Decode the actual value. for (int j = 0; j < num_components; ++j) { uint32_t val; diff --git a/contrib/draco/src/draco/compression/expert_encode.cc b/contrib/draco/src/draco/compression/expert_encode.cc index f9aec15eb..a3e649193 100644 --- a/contrib/draco/src/draco/compression/expert_encode.cc +++ b/contrib/draco/src/draco/compression/expert_encode.cc @@ -14,6 +14,12 @@ // #include "draco/compression/expert_encode.h" +#include +#include +#include +#include +#include + #include "draco/compression/mesh/mesh_edgebreaker_encoder.h" #include "draco/compression/mesh/mesh_sequential_encoder.h" #ifdef DRACO_POINT_CLOUD_COMPRESSION_SUPPORTED @@ -21,6 +27,9 @@ #include "draco/compression/point_cloud/point_cloud_sequential_encoder.h" #endif +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/bit_utils.h" +#endif namespace draco { ExpertEncoder::ExpertEncoder(const PointCloud &point_cloud) @@ -101,6 +110,11 @@ Status ExpertEncoder::EncodePointCloudToBuffer(const PointCloud &pc, Status ExpertEncoder::EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer) { +#ifdef DRACO_TRANSCODER_SUPPORTED + // Apply DracoCompressionOptions associated with the mesh. + DRACO_RETURN_IF_ERROR(ApplyCompressionOptions(m)); +#endif // DRACO_TRANSCODER_SUPPORTED + std::unique_ptr encoder; // Select the encoding method only based on the provided options. int encoding_method = options().GetGlobalInt("encoding_method", -1); @@ -118,6 +132,7 @@ Status ExpertEncoder::EncodeMeshToBuffer(const Mesh &m, encoder = std::unique_ptr(new MeshSequentialEncoder()); } encoder->SetMesh(m); + DRACO_RETURN_IF_ERROR(encoder->Encode(options(), out_buffer)); set_num_encoded_points(encoder->num_encoded_points()); @@ -179,4 +194,107 @@ Status ExpertEncoder::SetAttributePredictionScheme( return status; } +#ifdef DRACO_TRANSCODER_SUPPORTED +Status ExpertEncoder::ApplyCompressionOptions(const Mesh &mesh) { + if (!mesh.IsCompressionEnabled()) { + return OkStatus(); + } + const auto &compression_options = mesh.GetCompressionOptions(); + + // Set any encoder options that haven't been explicitly set by users (don't + // override existing options). + if (!options().IsSpeedSet()) { + options().SetSpeed(10 - compression_options.compression_level, + 10 - compression_options.compression_level); + } + + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + if (options().IsAttributeOptionSet(ai, "quantization_bits")) { + continue; // Don't override options that have been set. + } + int quantization_bits = 0; + const auto type = mesh.attribute(ai)->attribute_type(); + switch (type) { + case GeometryAttribute::POSITION: + if (compression_options.quantization_position + .AreQuantizationBitsDefined()) { + quantization_bits = + compression_options.quantization_position.quantization_bits(); + } else { + DRACO_RETURN_IF_ERROR(ApplyGridQuantization(mesh, ai)); + } + break; + case GeometryAttribute::TEX_COORD: + quantization_bits = compression_options.quantization_bits_tex_coord; + break; + case GeometryAttribute::NORMAL: + quantization_bits = compression_options.quantization_bits_normal; + break; + case GeometryAttribute::COLOR: + quantization_bits = compression_options.quantization_bits_color; + break; + case GeometryAttribute::TANGENT: + quantization_bits = compression_options.quantization_bits_tangent; + break; + case GeometryAttribute::WEIGHTS: + quantization_bits = compression_options.quantization_bits_weight; + break; + case GeometryAttribute::GENERIC: + quantization_bits = compression_options.quantization_bits_generic; + break; + default: + break; + } + if (quantization_bits > 0) { + options().SetAttributeInt(ai, "quantization_bits", quantization_bits); + } + } + return OkStatus(); +} + +Status ExpertEncoder::ApplyGridQuantization(const Mesh &mesh, + int attribute_index) { + const auto compression_options = mesh.GetCompressionOptions(); + if (mesh.attribute(attribute_index)->num_components() != 3) { + return ErrorStatus( + "Invalid number of components: Grid quantization is currently " + "supported only for 3D positions."); + } + const float spacing = compression_options.quantization_position.spacing(); + // Compute quantization properties based on the grid spacing. + const auto &bbox = mesh.ComputeBoundingBox(); + // Snap min and max points of the |bbox| to the quantization grid vertices. + Vector3f min_pos; + int num_values = 0; // Number of values that we need to encode. + for (int c = 0; c < 3; ++c) { + // Min / max position on grid vertices in grid coordinates. + const float min_grid_pos = floor(bbox.GetMinPoint()[c] / spacing); + const float max_grid_pos = ceil(bbox.GetMaxPoint()[c] / spacing); + + // Min pos on grid vertex in mesh coordinates. + min_pos[c] = min_grid_pos * spacing; + + const float component_num_values = + static_cast(max_grid_pos) - static_cast(min_grid_pos) + 1; + if (component_num_values > num_values) { + num_values = component_num_values; + } + } + // Now compute the number of bits needed to encode |num_values|. + int bits = MostSignificantBit(num_values); + if ((1 << bits) < num_values) { + // If the |num_values| is larger than number of values representable by + // |bits|, we need to use one more bit. This will be almost always true + // unless |num_values| was equal to 1 << |bits|. + bits++; + } + // Compute the range in mesh coordinates that matches the quantization bits. + // Note there are n-1 intervals between the |n| quantization values. + const float range = ((1 << bits) - 1) * spacing; + SetAttributeExplicitQuantization(attribute_index, bits, 3, min_pos.data(), + range); + return OkStatus(); +} +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace draco diff --git a/contrib/draco/src/draco/compression/expert_encode.h b/contrib/draco/src/draco/compression/expert_encode.h index ea59393d3..5c1485e1e 100644 --- a/contrib/draco/src/draco/compression/expert_encode.h +++ b/contrib/draco/src/draco/compression/expert_encode.h @@ -138,6 +138,12 @@ class ExpertEncoder : public EncoderBase { Status EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Applies compression options stored in |mesh|. + Status ApplyCompressionOptions(const Mesh &mesh); + Status ApplyGridQuantization(const Mesh &mesh, int attribute_index); +#endif // DRACO_TRANSCODER_SUPPORTED + const PointCloud *point_cloud_; const Mesh *mesh_; }; diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc index 0bbbea4af..21ad9959c 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_decoder_impl.cc @@ -454,7 +454,7 @@ bool MeshEdgebreakerDecoderImpl::DecodeConnectivity() { #endif // Decode connectivity of non-position attributes. - if (attribute_data_.size() > 0) { + if (!attribute_data_.empty()) { #ifdef DRACO_BACKWARDS_COMPATIBILITY_SUPPORTED if (decoder_->bitstream_version() < DRACO_BITSTREAM_VERSION(2, 1)) { for (CornerIndex ci(0); ci < corner_table_->num_corners(); ci += 3) { @@ -484,7 +484,10 @@ bool MeshEdgebreakerDecoderImpl::DecodeConnectivity() { attribute_data_[i].connectivity_data.AddSeamEdge(CornerIndex(c)); } // Recompute vertices from the newly added seam edges. - attribute_data_[i].connectivity_data.RecomputeVertices(nullptr, nullptr); + if (!attribute_data_[i].connectivity_data.RecomputeVertices(nullptr, + nullptr)) { + return false; + } } pos_encoding_data_.Init(corner_table_->num_vertices()); @@ -574,6 +577,17 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( const CornerIndex corner_b = corner_table_->Next(corner_table_->LeftMostCorner(vertex_x)); + if (corner_a == corner_b) { + // All matched corners must be different. + return -1; + } + if (corner_table_->Opposite(corner_a) != kInvalidCornerIndex || + corner_table_->Opposite(corner_b) != kInvalidCornerIndex) { + // One of the corners is already opposite to an existing face, which + // should not happen unless the input was tampered with. + return -1; + } + // New tip corner. const CornerIndex corner(3 * face.value()); // Update opposite corner mappings. @@ -616,6 +630,11 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( return -1; } const CornerIndex corner_a = active_corner_stack.back(); + if (corner_table_->Opposite(corner_a) != kInvalidCornerIndex) { + // Active corner is already opposite to an existing face, which should + // not happen unless the input was tampered with. + return -1; + } // First corner on the new face is either corner "l" or "r". const CornerIndex corner(3 * face.value()); @@ -681,10 +700,14 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( } const CornerIndex corner_a = active_corner_stack.back(); + if (corner_a == corner_b) { + // All matched corners must be different. + return -1; + } if (corner_table_->Opposite(corner_a) != kInvalidCornerIndex || corner_table_->Opposite(corner_b) != kInvalidCornerIndex) { // One of the corners is already opposite to an existing face, which - // should not happen unless the input was tempered with. + // should not happen unless the input was tampered with. return -1; } @@ -713,9 +736,15 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( // Also update the vertex id at corner "n" and all corners that are // connected to it in the CCW direction. + const CornerIndex first_corner = corner_n; while (corner_n != kInvalidCornerIndex) { corner_table_->MapCornerToVertex(corner_n, vertex_p); corner_n = corner_table_->SwingLeft(corner_n); + if (corner_n == first_corner) { + // We reached the start again which should not happen for split + // symbols. + return -1; + } } // Make sure the old vertex n is now mapped to an invalid corner (make it // isolated). @@ -800,7 +829,7 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( return -1; // Unexpected number of decoded vertices. } // Decode start faces and connect them to the faces from the active stack. - while (active_corner_stack.size() > 0) { + while (!active_corner_stack.empty()) { const CornerIndex corner = active_corner_stack.back(); active_corner_stack.pop_back(); const bool interior_face = @@ -842,6 +871,18 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( const CornerIndex corner_c = corner_table_->Next(corner_table_->LeftMostCorner(vert_x)); + if (corner == corner_b || corner == corner_c || corner_b == corner_c) { + // All matched corners must be different. + return -1; + } + if (corner_table_->Opposite(corner) != kInvalidCornerIndex || + corner_table_->Opposite(corner_b) != kInvalidCornerIndex || + corner_table_->Opposite(corner_c) != kInvalidCornerIndex) { + // One of the corners is already opposite to an existing face, which + // should not happen unless the input was tampered with. + return -1; + } + const VertexIndex vert_p = corner_table_->Vertex(corner_table_->Next(corner_c)); @@ -894,6 +935,11 @@ int MeshEdgebreakerDecoderImpl::DecodeConnectivity( VertexCornersIterator vcit(corner_table_.get(), src_vert); for (; !vcit.End(); ++vcit) { const CornerIndex cid = vcit.Corner(); + if (corner_table_->Vertex(cid) != src_vert) { + // Vertex mapped to |cid| was not |src_vert|. This indicates corrupted + // data and we should terminate the decoding. + return -1; + } corner_table_->MapCornerToVertex(cid, invalid_vert); } corner_table_->SetLeftMostCorner(invalid_vert, diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc index 5aff5d8cc..a7f381480 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder.cc @@ -31,7 +31,6 @@ bool MeshEdgebreakerEncoder::InitializeEncoder() { impl_ = nullptr; // For tiny meshes it's usually better to use the basic edgebreaker as the // overhead of the predictive one may turn out to be too big. - // TODO(b/111065939): Check if this can be improved. const bool is_tiny_mesh = mesh()->num_faces() < 1000; int selected_edgebreaker_method = diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc index 0791dc670..4bf6aa920 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.cc @@ -408,7 +408,7 @@ Status MeshEdgebreakerEncoderImpl::EncodeConnectivity() { init_face_connectivity_corners.begin(), init_face_connectivity_corners.end()); // Encode connectivity for all non-position attributes. - if (attribute_data_.size() > 0) { + if (!attribute_data_.empty()) { // Use the same order of corner that will be used by the decoder. visited_faces_.assign(mesh_->num_faces(), false); for (CornerIndex ci : processed_connectivity_corners_) { diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h index fb3377163..979e1d373 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoder_impl.h @@ -177,7 +177,6 @@ class MeshEdgebreakerEncoderImpl : public MeshEdgebreakerEncoderImplInterface { uint32_t num_split_symbols_; // Struct holding data used for encoding each non-position attribute. - // TODO(ostava): This should be probably renamed to something better. struct AttributeData { AttributeData() : attribute_index(-1), is_connectivity_used(true) {} int attribute_index; diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc index 831388245..523303b09 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_encoding_test.cc @@ -44,7 +44,7 @@ class MeshEdgebreakerEncodingTest : public ::testing::Test { EncoderOptions encoder_options = EncoderOptions::CreateDefaultOptions(); encoder_options.SetSpeed(10 - compression_level, 10 - compression_level); encoder.SetMesh(*mesh); - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer).ok()); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -52,15 +52,14 @@ class MeshEdgebreakerEncodingTest : public ::testing::Test { std::unique_ptr decoded_mesh(new Mesh()); DecoderOptions dec_options; - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_mesh.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_mesh.get())); // Cleanup the input mesh to make sure that input and output can be // compared (edgebreaker method discards degenerated triangles and isolated // vertices). const MeshCleanupOptions options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh, options)) << "Failed to clean the input mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh, options)); MeshAreEquivalent eq; ASSERT_TRUE(eq(*mesh, *decoded_mesh.get())) @@ -102,8 +101,8 @@ TEST_F(MeshEdgebreakerEncodingTest, TestEncoderReuse) { EncoderOptions encoder_options = EncoderOptions::CreateDefaultOptions(); encoder.SetMesh(*mesh); EncoderBuffer buffer_0, buffer_1; - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer_0).ok()); - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer_1).ok()); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer_0)); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer_1)); // Make sure both buffer are identical. ASSERT_EQ(buffer_0.size(), buffer_1.size()); @@ -123,7 +122,7 @@ TEST_F(MeshEdgebreakerEncodingTest, TestDecoderReuse) { EncoderOptions encoder_options = EncoderOptions::CreateDefaultOptions(); encoder.SetMesh(*mesh); EncoderBuffer buffer; - ASSERT_TRUE(encoder.Encode(encoder_options, &buffer).ok()); + DRACO_ASSERT_OK(encoder.Encode(encoder_options, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -133,13 +132,13 @@ TEST_F(MeshEdgebreakerEncodingTest, TestDecoderReuse) { // Decode the mesh two times. std::unique_ptr decoded_mesh_0(new Mesh()); DecoderOptions dec_options; - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_mesh_0.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_mesh_0.get())); dec_buffer.Init(buffer.data(), buffer.size()); std::unique_ptr decoded_mesh_1(new Mesh()); - ASSERT_TRUE( - decoder.Decode(dec_options, &dec_buffer, decoded_mesh_1.get()).ok()); + DRACO_ASSERT_OK( + decoder.Decode(dec_options, &dec_buffer, decoded_mesh_1.get())); // Make sure both of the meshes are identical. MeshAreEquivalent eq; @@ -169,7 +168,7 @@ TEST_F(MeshEdgebreakerEncodingTest, TestSingleConnectivityEncoding) { encoder.SetAttributeQuantization(GeometryAttribute::TEX_COORD, 8); encoder.SetAttributeQuantization(GeometryAttribute::NORMAL, 8); encoder.SetEncodingMethod(MESH_EDGEBREAKER_ENCODING); - ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -216,7 +215,7 @@ TEST_F(MeshEdgebreakerEncodingTest, TestWrongAttributeOrder) { encoder.SetAttributeQuantization(GeometryAttribute::POSITION, 8); encoder.SetAttributeQuantization(GeometryAttribute::NORMAL, 8); encoder.SetEncodingMethod(MESH_EDGEBREAKER_ENCODING); - ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok()); + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(*mesh, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h index cb3c29dd6..c650bc352 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_shared.h @@ -50,8 +50,6 @@ namespace draco { // \ / S \ / / E \ // *-------* *-------* // -// TODO(ostava): Get rid of the topology bit pattern. It's important only for -// encoding but the algorithms should use EdgebreakerSymbol instead. enum EdgebreakerTopologyBitPattern { TOPOLOGY_C = 0x0, // 0 TOPOLOGY_S = 0x1, // 1 0 0 diff --git a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h index c00373727..89553e909 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_edgebreaker_traversal_valence_decoder.h @@ -129,7 +129,11 @@ class MeshEdgebreakerTraversalValenceDecoder if (context_counter < 0) { return TOPOLOGY_INVALID; } - const int symbol_id = context_symbols_[active_context_][context_counter]; + const uint32_t symbol_id = + context_symbols_[active_context_][context_counter]; + if (symbol_id > 4) { + return TOPOLOGY_INVALID; + } last_symbol_ = edge_breaker_symbol_to_topology_id[symbol_id]; } else { #ifdef DRACO_BACKWARDS_COMPATIBILITY_SUPPORTED diff --git a/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc b/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc index 55f683696..2dfdb58ef 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_encoder_test.cc @@ -78,9 +78,10 @@ class MeshEncoderTest : public ::testing::TestWithParam { encoder.SetAttributeQuantization(i, 12); } EncoderBuffer buffer; - ASSERT_TRUE(encoder.EncodeToBuffer(&buffer).ok()) - << "Failed encoding test mesh " << file_name << " with method " - << GetParam().encoding_method; + const Status status = encoder.EncodeToBuffer(&buffer); + EXPECT_TRUE(status.ok()) << "Failed encoding test mesh " << file_name + << " with method " << GetParam().encoding_method; + DRACO_ASSERT_OK(status); // Check that the encoded mesh was really encoded with the selected method. DecoderBuffer decoder_buffer; decoder_buffer.Init(buffer.data(), buffer.size()); @@ -88,6 +89,7 @@ class MeshEncoderTest : public ::testing::TestWithParam { uint8_t encoded_method; ASSERT_TRUE(decoder_buffer.Decode(&encoded_method)); ASSERT_EQ(encoded_method, method); + if (!FLAGS_update_golden_files) { EXPECT_TRUE( CompareGoldenFile(golden_file_name, buffer.data(), buffer.size())) diff --git a/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc b/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc index be349f543..595a487a4 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_sequential_decoder.cc @@ -96,7 +96,7 @@ bool MeshSequentialDecoder::DecodeConnectivity() { } mesh()->AddFace(face); } - } else if (mesh()->num_points() < (1 << 21) && + } else if (num_points < (1 << 21) && bitstream_version() >= DRACO_BITSTREAM_VERSION(2, 2)) { // Decode indices as uint32_t. for (uint32_t i = 0; i < num_faces; ++i) { @@ -158,6 +158,10 @@ bool MeshSequentialDecoder::DecodeAndDecompressIndices(uint32_t num_faces) { index_diff = -index_diff; } const int32_t index_value = index_diff + last_index_value; + if (index_value < 0) { + // Negative indices are not allowed. + return false; + } face[j] = index_value; last_index_value = index_value; } diff --git a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc index 02ac7779e..fd8b11392 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc +++ b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.cc @@ -32,8 +32,6 @@ Status MeshSequentialEncoder::EncodeConnectivity() { EncodeVarint(static_cast(mesh()->num_points()), buffer()); // We encode all attributes in the original (possibly duplicated) format. - // TODO(ostava): This may not be optimal if we have only one attribute or if - // all attributes share the same index mapping. if (options()->GetGlobalBool("compress_connectivity", false)) { // 0 = Encode compressed indices. buffer()->Encode(static_cast(0)); @@ -44,8 +42,6 @@ Status MeshSequentialEncoder::EncodeConnectivity() { // 1 = Encode indices directly. buffer()->Encode(static_cast(1)); // Store vertex indices using a smallest data type that fits their range. - // TODO(ostava): This can be potentially improved by using a tighter - // fit that is not bound by a bit-length of any particular data type. if (mesh()->num_points() < 256) { // Serialize indices as uint8_t. for (FaceIndex i(0); i < num_faces; ++i) { diff --git a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h index 672609642..6e2b05877 100644 --- a/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h +++ b/contrib/draco/src/draco/compression/mesh/mesh_sequential_encoder.h @@ -33,7 +33,6 @@ namespace draco { // Class that encodes mesh data using a simple binary representation of mesh's // connectivity and geometry. -// TODO(ostava): Use a better name. class MeshSequentialEncoder : public MeshEncoder { public: MeshSequentialEncoder(); diff --git a/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h b/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h index e66dd14b2..dd9738ba2 100644 --- a/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h +++ b/contrib/draco/src/draco/compression/mesh/traverser/mesh_attribute_indices_encoding_observer.h @@ -25,7 +25,7 @@ namespace draco { // values based on the traversal of the encoded mesh. The class should be used // as the TraversalObserverT member of a Traverser class such as the // DepthFirstTraverser (depth_first_traverser.h). -// TODO(hemmer): rename to AttributeIndicesCodingTraverserObserver +// TODO(b/199760123): Rename to AttributeIndicesCodingTraverserObserver. template class MeshAttributeIndicesEncodingObserver { public: diff --git a/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h b/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h index ebe1d5f7a..e55c93a79 100644 --- a/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h +++ b/contrib/draco/src/draco/compression/mesh/traverser/mesh_traversal_sequencer.h @@ -25,7 +25,7 @@ namespace draco { // Sequencer that generates point sequence in an order given by a deterministic // traversal on the mesh surface. Note that all attributes encoded with this // sequence must share the same connectivity. -// TODO(hemmer): Consider refactoring such that this is an observer. +// TODO(b/199760123): Consider refactoring such that this is an observer. template class MeshTraversalSequencer : public PointsSequencer { public: diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h index 87bc2b7ef..55bafe7c4 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_decoder.h @@ -18,8 +18,10 @@ #define DRACO_COMPRESSION_POINT_CLOUD_ALGORITHMS_DYNAMIC_INTEGER_POINTS_KD_TREE_DECODER_H_ #include +#include #include #include +#include #include "draco/compression/bit_coders/adaptive_rans_bit_decoder.h" #include "draco/compression/bit_coders/direct_bit_decoder.h" @@ -92,17 +94,29 @@ class DynamicIntegerPointsKdTreeDecoder { base_stack_(32 * dimension + 1, VectorUint32(dimension, 0)), levels_stack_(32 * dimension + 1, VectorUint32(dimension, 0)) {} - // Decodes a integer point cloud from |buffer|. + // Decodes an integer point cloud from |buffer|. Optional |oit_max_points| can + // be used to tell the decoder the maximum number of points accepted by the + // iterator. template bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &oit); + template + bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &oit, + uint32_t oit_max_points); + #ifndef DRACO_OLD_GCC template bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &&oit); + template + bool DecodePoints(DecoderBuffer *buffer, OutputIteratorT &&oit, + uint32_t oit_max_points); #endif // DRACO_OLD_GCC const uint32_t dimension() const { return dimension_; } + // Returns the number of decoded points. Must be called after DecodePoints(). + uint32_t num_decoded_points() const { return num_decoded_points_; } + private: uint32_t GetAxis(uint32_t num_remaining_points, const VectorUint32 &levels, uint32_t last_axis); @@ -146,8 +160,15 @@ template template bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( DecoderBuffer *buffer, OutputIteratorT &&oit) { + return DecodePoints(buffer, oit, std::numeric_limits::max()); +} + +template +template +bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( + DecoderBuffer *buffer, OutputIteratorT &&oit, uint32_t oit_max_points) { OutputIteratorT local = std::forward(oit); - return DecodePoints(buffer, local); + return DecodePoints(buffer, local, oit_max_points); } #endif // DRACO_OLD_GCC @@ -155,6 +176,13 @@ template template bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( DecoderBuffer *buffer, OutputIteratorT &oit) { + return DecodePoints(buffer, oit, std::numeric_limits::max()); +} + +template +template +bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( + DecoderBuffer *buffer, OutputIteratorT &oit, uint32_t oit_max_points) { if (!buffer->Decode(&bit_length_)) { return false; } @@ -167,6 +195,9 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodePoints( if (num_points_ == 0) { return true; } + if (num_points_ > oit_max_points) { + return false; + } num_decoded_points_ = 0; if (!numbers_decoder_.StartDecoding(buffer)) { @@ -227,7 +258,7 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( std::stack status_stack; status_stack.push(init_status); - // TODO(hemmer): use preallocated vector instead of stack. + // TODO(b/199760123): Use preallocated vector instead of stack. while (!status_stack.empty()) { const DecodingStatus status = status_stack.top(); status_stack.pop(); @@ -263,7 +294,8 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( // Fast decoding of remaining bits if number of points is 1 or 2. if (num_remaining_points <= 2) { - // TODO(hemmer): axes_ not necessary, remove would change bitstream! + // TODO(b/199760123): |axes_| not necessary, remove would change + // bitstream! axes_[0] = axis; for (uint32_t i = 1; i < dimension_; i++) { axes_[i] = DRACO_INCREMENT_MOD(axes_[i - 1], dimension_); @@ -273,8 +305,10 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( p_[axes_[j]] = 0; const uint32_t num_remaining_bits = bit_length_ - levels[axes_[j]]; if (num_remaining_bits) { - remaining_bits_decoder_.DecodeLeastSignificantBits32( - num_remaining_bits, &p_[axes_[j]]); + if (!remaining_bits_decoder_.DecodeLeastSignificantBits32( + num_remaining_bits, &p_[axes_[j]])) { + return false; + } } p_[axes_[j]] = old_base[axes_[j]] | p_[axes_[j]]; } @@ -299,7 +333,12 @@ bool DynamicIntegerPointsKdTreeDecoder::DecodeInternal( uint32_t number = 0; DecodeNumber(incoming_bits, &number); - uint32_t first_half = num_remaining_points / 2 - number; + uint32_t first_half = num_remaining_points / 2; + if (first_half < number) { + // Invalid |number|. + return false; + } + first_half -= number; uint32_t second_half = num_remaining_points - first_half; if (first_half != second_half) { diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h index 14fa32d70..65b3d07a6 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/dynamic_integer_points_kd_tree_encoder.h @@ -280,7 +280,7 @@ void DynamicIntegerPointsKdTreeEncoder::EncodeInternal( std::stack status_stack; status_stack.push(init_status); - // TODO(hemmer): use preallocated vector instead of stack. + // TODO(b/199760123): Use preallocated vector instead of stack. while (!status_stack.empty()) { Status status = status_stack.top(); status_stack.pop(); @@ -305,7 +305,8 @@ void DynamicIntegerPointsKdTreeEncoder::EncodeInternal( // Fast encoding of remaining bits if number of points is 1 or 2. // Doing this also for 2 gives a slight additional speed up. if (num_remaining_points <= 2) { - // TODO(hemmer): axes_ not necessary, remove would change bitstream! + // TODO(b/199760123): |axes_| not necessary, remove would change + // bitstream! axes_[0] = axis; for (uint32_t i = 1; i < dimension_; i++) { axes_[i] = DRACO_INCREMENT_MOD(axes_[i - 1], dimension_); diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h index 26ba94f1f..44c1b3d3a 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/float_points_tree_encoder.h @@ -44,7 +44,7 @@ namespace draco { // there are more leading zeros, which is then compressed better by the // arithmetic encoding. -// TODO(hemmer): Remove class because it duplicates quantization code. +// TODO(b/199760123): Remove class because it duplicates quantization code. class FloatPointsTreeEncoder { public: explicit FloatPointsTreeEncoder(PointCloudCompressionMethod method); @@ -91,7 +91,7 @@ bool FloatPointsTreeEncoder::EncodePointCloud(InputIteratorT points_begin, // Collect necessary data for encoding. num_points_ = std::distance(points_begin, points_end); - // TODO(hemmer): Extend quantization tools to make this more automatic. + // TODO(b/199760123): Extend quantization tools to make this more automatic. // Compute range of points for quantization std::vector qpoints; qpoints.reserve(num_points_); diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h index 94e523cad..bc31af586 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_decoder.h @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// TODO(hemmer): Make this a wrapper using DynamicIntegerPointsKdTreeDecoder. +// TODO(b/199760123): Make this a wrapper using +// DynamicIntegerPointsKdTreeDecoder. // // See integer_points_kd_tree_encoder.h for documentation. diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h index b8811092e..654f14a78 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/integer_points_kd_tree_encoder.h @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// TODO(hemmer): Make this a wrapper using DynamicIntegerPointsKdTreeEncoder. +// TODO(b/199760123): Make this a wrapper using +// DynamicIntegerPointsKdTreeEncoder. #ifndef DRACO_COMPRESSION_POINT_CLOUD_ALGORITHMS_INTEGER_POINTS_KD_TREE_ENCODER_H_ #define DRACO_COMPRESSION_POINT_CLOUD_ALGORITHMS_INTEGER_POINTS_KD_TREE_ENCODER_H_ diff --git a/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h b/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h index 01943ad9e..8ea0741da 100644 --- a/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h +++ b/contrib/draco/src/draco/compression/point_cloud/algorithms/quantize_points_3.h @@ -22,7 +22,7 @@ namespace draco { -// TODO(hemmer): Make this a stable bounding box. +// TODO(b/199760123): Make this a stable bounding box. struct QuantizationInfo { uint32_t quantization_bits; float range; @@ -41,7 +41,7 @@ OutputIterator QuantizePoints3(const PointIterator &begin, max_range = std::max(std::fabs((*it)[2]), max_range); } - const uint32_t max_quantized_value((1 << info->quantization_bits) - 1); + const uint32_t max_quantized_value((1u << info->quantization_bits) - 1); Quantizer quantize; quantize.Init(max_range, max_quantized_value); info->range = max_range; @@ -66,7 +66,7 @@ void DequantizePoints3(const QPointIterator &begin, const QPointIterator &end, const uint32_t quantization_bits = info.quantization_bits; const float range = info.range; - const uint32_t max_quantized_value((1 << quantization_bits) - 1); + const uint32_t max_quantized_value((1u << quantization_bits) - 1); Dequantizer dequantize; dequantize.Init(range, max_quantized_value); diff --git a/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc b/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc index 2249bb09e..7a7b597f2 100644 --- a/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc +++ b/contrib/draco/src/draco/compression/point_cloud/point_cloud_kd_tree_encoding_test.cc @@ -68,7 +68,7 @@ class PointCloudKdTreeEncodingTest : public ::testing::Test { ++compression_level) { options.SetSpeed(10 - compression_level, 10 - compression_level); encoder.SetPointCloud(pc); - ASSERT_TRUE(encoder.Encode(options, &buffer).ok()); + DRACO_ASSERT_OK(encoder.Encode(options, &buffer)); DecoderBuffer dec_buffer; dec_buffer.Init(buffer.data(), buffer.size()); @@ -76,7 +76,7 @@ class PointCloudKdTreeEncodingTest : public ::testing::Test { std::unique_ptr out_pc(new PointCloud()); DecoderOptions dec_options; - ASSERT_TRUE(decoder.Decode(dec_options, &dec_buffer, out_pc.get()).ok()); + DRACO_ASSERT_OK(decoder.Decode(dec_options, &dec_buffer, out_pc.get())); ComparePointClouds(pc, *out_pc); } diff --git a/contrib/draco/src/draco/core/bounding_box.cc b/contrib/draco/src/draco/core/bounding_box.cc index 8a0709678..8acd6687b 100644 --- a/contrib/draco/src/draco/core/bounding_box.cc +++ b/contrib/draco/src/draco/core/bounding_box.cc @@ -20,11 +20,20 @@ BoundingBox::BoundingBox() : BoundingBox(Vector3f(std::numeric_limits::max(), std::numeric_limits::max(), std::numeric_limits::max()), - Vector3f(-std::numeric_limits::max(), - -std::numeric_limits::max(), - -std::numeric_limits::max())) {} + Vector3f(std::numeric_limits::lowest(), + std::numeric_limits::lowest(), + std::numeric_limits::lowest())) {} BoundingBox::BoundingBox(const Vector3f &min_point, const Vector3f &max_point) : min_point_(min_point), max_point_(max_point) {} +const bool BoundingBox::IsValid() const { + return GetMinPoint()[0] != std::numeric_limits::max() && + GetMinPoint()[1] != std::numeric_limits::max() && + GetMinPoint()[2] != std::numeric_limits::max() && + GetMaxPoint()[0] != std::numeric_limits::lowest() && + GetMaxPoint()[1] != std::numeric_limits::lowest() && + GetMaxPoint()[2] != std::numeric_limits::lowest(); +} + } // namespace draco diff --git a/contrib/draco/src/draco/core/bounding_box.h b/contrib/draco/src/draco/core/bounding_box.h index 31ba2d683..697a73b6f 100644 --- a/contrib/draco/src/draco/core/bounding_box.h +++ b/contrib/draco/src/draco/core/bounding_box.h @@ -38,6 +38,11 @@ class BoundingBox { // Returns the maximum point of the bounding box. inline const Vector3f &GetMaxPoint() const { return max_point_; } + // Checks if the bounding box object was created with the default constructor + // then never updated. Internally, checks if the bounding box minimum and + // maximum points hold the largest positive and smallest negative values. + const bool IsValid() const; + // Conditionally updates the bounding box with a given |new_point|. void Update(const Vector3f &new_point) { for (int i = 0; i < 3; i++) { diff --git a/contrib/draco/src/draco/core/constants.h b/contrib/draco/src/draco/core/constants.h new file mode 100644 index 000000000..3e81992a1 --- /dev/null +++ b/contrib/draco/src/draco/core/constants.h @@ -0,0 +1,6 @@ +#ifndef DRACO_CORE_CONSTANTS_H_ +#define DRACO_CORE_CONSTANTS_H_ + +#define DRACO_PI 3.14159265358979323846 + +#endif // DRACO_CORE_CONSTANTS_H_ diff --git a/contrib/draco/src/draco/core/data_buffer.cc b/contrib/draco/src/draco/core/data_buffer.cc index f0b43d67d..96a378798 100644 --- a/contrib/draco/src/draco/core/data_buffer.cc +++ b/contrib/draco/src/draco/core/data_buffer.cc @@ -52,7 +52,7 @@ void DataBuffer::Resize(int64_t size) { } void DataBuffer::WriteDataToStream(std::ostream &stream) { - if (data_.size() == 0) { + if (data_.empty()) { return; } stream.write(reinterpret_cast(data_.data()), data_.size()); diff --git a/contrib/draco/src/draco/core/data_buffer.h b/contrib/draco/src/draco/core/data_buffer.h index 8ee690540..8eac0f6b4 100644 --- a/contrib/draco/src/draco/core/data_buffer.h +++ b/contrib/draco/src/draco/core/data_buffer.h @@ -67,7 +67,7 @@ class DataBuffer { int64_t update_count() const { return descriptor_.buffer_update_count; } size_t data_size() const { return data_.size(); } const uint8_t *data() const { return data_.data(); } - uint8_t *data() { return &data_[0]; } + uint8_t *data() { return data_.data(); } int64_t buffer_id() const { return descriptor_.buffer_id; } void set_buffer_id(int64_t buffer_id) { descriptor_.buffer_id = buffer_id; } diff --git a/contrib/draco/src/draco/core/decoder_buffer.h b/contrib/draco/src/draco/core/decoder_buffer.h index 0559abbe4..71189b7e7 100644 --- a/contrib/draco/src/draco/core/decoder_buffer.h +++ b/contrib/draco/src/draco/core/decoder_buffer.h @@ -54,12 +54,11 @@ class DecoderBuffer { // Decodes up to 32 bits into out_val. Can be called only in between // StartBitDecoding and EndBitDecoding. Otherwise returns false. - bool DecodeLeastSignificantBits32(int nbits, uint32_t *out_value) { + bool DecodeLeastSignificantBits32(uint32_t nbits, uint32_t *out_value) { if (!bit_decoder_active()) { return false; } - bit_decoder_.GetBits(nbits, out_value); - return true; + return bit_decoder_.GetBits(nbits, out_value); } // Decodes an arbitrary data type. @@ -158,11 +157,12 @@ class DecoderBuffer { inline void ConsumeBits(int k) { bit_offset_ += k; } // Returns |nbits| bits in |x|. - inline bool GetBits(int32_t nbits, uint32_t *x) { - DRACO_DCHECK_GE(nbits, 0); - DRACO_DCHECK_LE(nbits, 32); + inline bool GetBits(uint32_t nbits, uint32_t *x) { + if (nbits > 32) { + return false; + } uint32_t value = 0; - for (int32_t bit = 0; bit < nbits; ++bit) { + for (uint32_t bit = 0; bit < nbits; ++bit) { value |= GetBit() << bit; } *x = value; diff --git a/contrib/draco/src/draco/core/draco_index_type_vector.h b/contrib/draco/src/draco/core/draco_index_type_vector.h index aae1e7aaf..f5256ded9 100644 --- a/contrib/draco/src/draco/core/draco_index_type_vector.h +++ b/contrib/draco/src/draco/core/draco_index_type_vector.h @@ -25,25 +25,32 @@ namespace draco { // A wrapper around the standard std::vector that supports indexing of the // vector entries using the strongly typed indices as defined in -// draco_index_type.h . -// TODO(ostava): Make the interface more complete. It's currently missing -// features such as iterators. -// TODO(vytyaz): Add more unit tests for this class. +// draco_index_type.h. +// TODO(ostava): Make the interface more complete. It's currently missing some +// features. template class IndexTypeVector { public: typedef typename std::vector::const_reference const_reference; typedef typename std::vector::reference reference; + typedef typename std::vector::iterator iterator; + typedef typename std::vector::const_iterator const_iterator; IndexTypeVector() {} explicit IndexTypeVector(size_t size) : vector_(size) {} IndexTypeVector(size_t size, const ValueTypeT &val) : vector_(size, val) {} + iterator begin() { return vector_.begin(); } + const_iterator begin() const { return vector_.begin(); } + iterator end() { return vector_.end(); } + const_iterator end() const { return vector_.end(); } + void clear() { vector_.clear(); } void reserve(size_t size) { vector_.reserve(size); } void resize(size_t size) { vector_.resize(size); } void resize(size_t size, const ValueTypeT &val) { vector_.resize(size, val); } void assign(size_t size, const ValueTypeT &val) { vector_.assign(size, val); } + iterator erase(iterator position) { return vector_.erase(position); } void swap(IndexTypeVector &arg) { vector_.swap(arg.vector_); diff --git a/contrib/draco/src/draco/core/draco_test_utils.cc b/contrib/draco/src/draco/core/draco_test_utils.cc index edca9856d..a71082a86 100644 --- a/contrib/draco/src/draco/core/draco_test_utils.cc +++ b/contrib/draco/src/draco/core/draco_test_utils.cc @@ -16,9 +16,9 @@ #include +#include "draco/core/draco_test_base.h" #include "draco/core/macros.h" #include "draco/io/file_utils.h" -#include "draco_test_base.h" namespace draco { @@ -27,6 +27,8 @@ static constexpr char kTestDataDir[] = DRACO_TEST_DATA_DIR; static constexpr char kTestTempDir[] = DRACO_TEST_TEMP_DIR; } // namespace +std::string GetTestTempDir() { return std::string(kTestDataDir); } + std::string GetTestFileFullPath(const std::string &file_name) { return std::string(kTestDataDir) + std::string("/") + file_name; } @@ -55,11 +57,13 @@ bool CompareGoldenFile(const std::string &golden_file_name, const void *data, size_t remaining_data_size = data_size; int offset = 0; while ((extracted_size = in_file.read(buffer, buffer_size).gcount()) > 0) { - if (remaining_data_size <= 0) + if (remaining_data_size <= 0) { break; // Input and golden sizes are different. + } size_t size_to_check = extracted_size; - if (remaining_data_size < size_to_check) + if (remaining_data_size < size_to_check) { size_to_check = remaining_data_size; + } for (uint32_t i = 0; i < size_to_check; ++i) { if (buffer[i] != data_c8[offset++]) { LOG(INFO) << "Test output differed from golden file at byte " @@ -77,4 +81,20 @@ bool CompareGoldenFile(const std::string &golden_file_name, const void *data, return true; } +#ifdef DRACO_TRANSCODER_SUPPORTED + +template <> +std::unique_ptr ReadGeometryFromTestFile( + const std::string &file_name) { + return ReadMeshFromTestFile(file_name); +} + +template <> +std::unique_ptr ReadGeometryFromTestFile( + const std::string &file_name) { + return ReadSceneFromTestFile(file_name); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace draco diff --git a/contrib/draco/src/draco/core/draco_test_utils.h b/contrib/draco/src/draco/core/draco_test_utils.h index fa548f52d..658096fe1 100644 --- a/contrib/draco/src/draco/core/draco_test_utils.h +++ b/contrib/draco/src/draco/core/draco_test_utils.h @@ -15,12 +15,24 @@ #ifndef DRACO_CORE_DRACO_TEST_UTILS_H_ #define DRACO_CORE_DRACO_TEST_UTILS_H_ +#include +#include +#include + #include "draco/core/draco_test_base.h" +#include "draco/draco_features.h" #include "draco/io/mesh_io.h" #include "draco/io/point_cloud_io.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/io/scene_io.h" +#endif + namespace draco { +// Returns test temporary directory. +std::string GetTestTempDir(); + // Returns the full path to a given file system entry, such as test file or test // directory. std::string GetTestFileFullPath(const std::string &entry_name); @@ -65,6 +77,47 @@ inline std::unique_ptr ReadPointCloudFromTestFile( return ReadPointCloudFromFile(path).value(); } +#ifdef DRACO_TRANSCODER_SUPPORTED +inline std::unique_ptr ReadSceneFromTestFile( + const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + return ReadSceneFromFile(path).value(); +} + +// Loads geometry specified by a |file_name| that is going to be automatically +// converted to the correct path available to the testing instance. Supported +// geometry types are Mesh and Scene. +template +std::unique_ptr ReadGeometryFromTestFile(const std::string &file_name); + +#endif // DRACO_TRANSCODER_SUPPORTED + +// Utility class for redirection and capture of stderr/stdout. +class CaptureStream { + public: + explicit CaptureStream(std::ostream &stream) + : old_buffer_(stream.rdbuf(buffer_.rdbuf())), stream_(stream) {} + + ~CaptureStream() { Reset(); } + + std::string GetStringAndRelease() { + Reset(); + return buffer_.str(); + } + + void Reset() { + if (old_buffer_) { + stream_.rdbuf(old_buffer_); + old_buffer_ = nullptr; + } + } + + private: + std::ostringstream buffer_; + std::streambuf *old_buffer_ = nullptr; + std::ostream &stream_; +}; + // Evaluates an expression that returns draco::Status. If the status is not OK, // the macro asserts and logs the error message. #define DRACO_ASSERT_OK(expression) \ diff --git a/contrib/draco/src/draco/core/draco_version.h b/contrib/draco/src/draco/core/draco_version.h index 14a504a50..88856447f 100644 --- a/contrib/draco/src/draco/core/draco_version.h +++ b/contrib/draco/src/draco/core/draco_version.h @@ -18,9 +18,7 @@ namespace draco { // Draco version is comprised of ... -static const char kDracoVersion[] = "1.4.1"; - -const char *Version() { return kDracoVersion; } +static const char kDracoVersion[] = "1.5.6"; } // namespace draco diff --git a/contrib/draco/src/draco/core/macros.h b/contrib/draco/src/draco/core/macros.h index 147bbaafc..a31e7c44b 100644 --- a/contrib/draco/src/draco/core/macros.h +++ b/contrib/draco/src/draco/core/macros.h @@ -15,7 +15,8 @@ #ifndef DRACO_CORE_MACROS_H_ #define DRACO_CORE_MACROS_H_ -#include "assert.h" +#include + #include "draco/draco_features.h" #ifdef ANDROID_LOGGING @@ -37,7 +38,7 @@ namespace draco { #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ TypeName(const TypeName &) = delete; \ void operator=(const TypeName &) = delete; -#endif +#endif // DISALLOW_COPY_AND_ASSIGN #ifndef FALLTHROUGH_INTENDED #if defined(__clang__) && defined(__has_warning) @@ -46,7 +47,7 @@ namespace draco { #endif #elif defined(__GNUC__) && __GNUC__ >= 7 #define FALLTHROUGH_INTENDED [[gnu::fallthrough]] -#endif +#endif // FALLTHROUGH_INTENDED // If FALLTHROUGH_INTENDED is still not defined, define it. #ifndef FALLTHROUGH_INTENDED @@ -54,7 +55,7 @@ namespace draco { do { \ } while (0) #endif -#endif +#endif // FALLTHROUGH_INTENDED #ifndef LOG #define LOG(...) std::cout @@ -84,12 +85,16 @@ namespace draco { #define DRACO_DCHECK_LE(a, b) #define DRACO_DCHECK_LT(a, b) #define DRACO_DCHECK_NOTNULL(x) -#endif +#endif // DRACO_DEBUG // Helper macros for concatenating macro values. #define DRACO_MACROS_IMPL_CONCAT_INNER_(x, y) x##y #define DRACO_MACROS_IMPL_CONCAT_(x, y) DRACO_MACROS_IMPL_CONCAT_INNER_(x, y) +#define DRACO_MACROS_IMPL_CONCAT_INNER_3_(x, y, z) x##y##z +#define DRACO_MACROS_IMPL_CONCAT_3_(x, y, z) \ + DRACO_MACROS_IMPL_CONCAT_INNER_3_(x, y, z) + // Expand the n-th argument of the macro. Used to select an argument based on // the number of entries in a variadic macro argument. Example usage: // @@ -100,9 +105,9 @@ namespace draco { // #define VARIADIC_MACRO(...) // DRACO_SELECT_NTH_FROM_3(__VA_ARGS__, FUNC_3, FUNC_2, FUNC_1) __VA_ARGS__ // -#define DRACO_SELECT_NTH_FROM_2(_1, _2, NAME) NAME -#define DRACO_SELECT_NTH_FROM_3(_1, _2, _3, NAME) NAME -#define DRACO_SELECT_NTH_FROM_4(_1, _2, _3, _4, NAME) NAME +#define DRACO_SELECT_NTH_FROM_2(_1, _2, NAME, ...) NAME +#define DRACO_SELECT_NTH_FROM_3(_1, _2, _3, NAME, ...) NAME +#define DRACO_SELECT_NTH_FROM_4(_1, _2, _3, _4, NAME, ...) NAME // Macro that converts the Draco bit-stream into one uint16_t number. // Useful mostly when checking version numbers. diff --git a/contrib/draco/src/draco/core/math_utils.h b/contrib/draco/src/draco/core/math_utils.h index 7f382fa34..d7732e55d 100644 --- a/contrib/draco/src/draco/core/math_utils.h +++ b/contrib/draco/src/draco/core/math_utils.h @@ -19,6 +19,8 @@ #include "draco/core/vector_d.h" +namespace draco { + #define DRACO_INCREMENT_MOD(I, M) (((I) == ((M)-1)) ? 0 : ((I) + 1)) // Returns floor(sqrt(x)) where x is an integer number. The main intend of this @@ -52,4 +54,26 @@ inline uint64_t IntSqrt(uint64_t number) { return square_root; } +// Performs the addition in unsigned type to avoid signed integer overflow. Note +// that the result will be the same (for non-overflowing values). +template < + typename DataTypeT, + typename std::enable_if::value && + std::is_signed::value>::type * = nullptr> +inline DataTypeT AddAsUnsigned(DataTypeT a, DataTypeT b) { + typedef typename std::make_unsigned::type DataTypeUT; + return static_cast(static_cast(a) + + static_cast(b)); +} + +template ::value || + !std::is_signed::value>::type * = + nullptr> +inline DataTypeT AddAsUnsigned(DataTypeT a, DataTypeT b) { + return a + b; +} + +} // namespace draco + #endif // DRACO_CORE_MATH_UTILS_H_ diff --git a/contrib/draco/src/draco/core/math_utils_test.cc b/contrib/draco/src/draco/core/math_utils_test.cc index 8c255d046..460a67444 100644 --- a/contrib/draco/src/draco/core/math_utils_test.cc +++ b/contrib/draco/src/draco/core/math_utils_test.cc @@ -5,7 +5,7 @@ #include "draco/core/draco_test_base.h" -using draco::Vector3f; +namespace draco { TEST(MathUtils, Mod) { EXPECT_EQ(DRACO_INCREMENT_MOD(1, 1 << 1), 0); } @@ -20,3 +20,5 @@ TEST(MathUtils, IntSqrt) { ASSERT_EQ(IntSqrt(number), static_cast(floor(std::sqrt(number)))); } } + +} // namespace draco diff --git a/contrib/draco/src/draco/core/options.cc b/contrib/draco/src/draco/core/options.cc index 9b81db489..ceb87cccc 100644 --- a/contrib/draco/src/draco/core/options.cc +++ b/contrib/draco/src/draco/core/options.cc @@ -15,13 +15,12 @@ #include "draco/core/options.h" #include +#include #include #include namespace draco { -Options::Options() {} - void Options::MergeAndReplace(const Options &other_options) { for (const auto &item : other_options.options_) { options_[item.first] = item.second; diff --git a/contrib/draco/src/draco/core/options.h b/contrib/draco/src/draco/core/options.h index 1bc4dc0fb..3f15d13ba 100644 --- a/contrib/draco/src/draco/core/options.h +++ b/contrib/draco/src/draco/core/options.h @@ -19,6 +19,8 @@ #include #include +#include "draco/draco_features.h" + namespace draco { // Class for storing generic options as a pair in a string map. @@ -27,7 +29,8 @@ namespace draco { // data type. class Options { public: - Options(); + Options() = default; + ~Options() = default; // Merges |other_options| on top of the existing options of this instance // replacing all entries that are present in both options instances. @@ -71,8 +74,6 @@ class Options { private: // All entries are internally stored as strings and converted to the desired // return type based on the used Get* method. - // TODO(ostava): Consider adding type safety mechanism that would prevent - // unsafe operations such as a conversion from vector to int. std::map options_; }; diff --git a/contrib/draco/src/draco/core/status.cc b/contrib/draco/src/draco/core/status.cc new file mode 100644 index 000000000..ecb0b536e --- /dev/null +++ b/contrib/draco/src/draco/core/status.cc @@ -0,0 +1,44 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include "draco/core/status.h" + +#include + +namespace draco { + +std::string Status::code_string() const { + switch (code_) { + case Code::OK: + return "OK"; + case Code::DRACO_ERROR: + return "DRACO_ERROR"; + case Code::IO_ERROR: + return "IO_ERROR"; + case Code::INVALID_PARAMETER: + return "INVALID_PARAMETER"; + case Code::UNSUPPORTED_VERSION: + return "UNSUPPORTED_VERSION"; + case Code::UNKNOWN_VERSION: + return "UNKNOWN_VERSION"; + case Code::UNSUPPORTED_FEATURE: + return "UNSUPPORTED_FEATURE"; + } + return "UNKNOWN_STATUS_VALUE"; +} + +std::string Status::code_and_error_string() const { + return code_string() + ": " + error_msg_string(); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/core/status.h b/contrib/draco/src/draco/core/status.h index 449ad8566..fac96046c 100644 --- a/contrib/draco/src/draco/core/status.h +++ b/contrib/draco/src/draco/core/status.h @@ -15,6 +15,7 @@ #ifndef DRACO_CORE_STATUS_H_ #define DRACO_CORE_STATUS_H_ +#include #include namespace draco { @@ -44,6 +45,8 @@ class Status { Code code() const { return code_; } const std::string &error_msg_string() const { return error_msg_; } const char *error_msg() const { return error_msg_.c_str(); } + std::string code_string() const; + std::string code_and_error_string() const; bool operator==(Code code) const { return code == code_; } bool ok() const { return code_ == OK; } @@ -61,6 +64,9 @@ inline std::ostream &operator<<(std::ostream &os, const Status &status) { } inline Status OkStatus() { return Status(Status::OK); } +inline Status ErrorStatus(const std::string &msg) { + return Status(Status::DRACO_ERROR, msg); +} // Evaluates an expression that returns draco::Status. If the status is not OK, // the macro returns the status object. diff --git a/contrib/draco/src/draco/core/status_test.cc b/contrib/draco/src/draco/core/status_test.cc index c1ad4ab30..dc36496d4 100644 --- a/contrib/draco/src/draco/core/status_test.cc +++ b/contrib/draco/src/draco/core/status_test.cc @@ -29,10 +29,17 @@ TEST_F(StatusTest, TestStatusOutput) { // Tests that the Status can be stored in a provided std::ostream. const draco::Status status(draco::Status::DRACO_ERROR, "Error msg."); ASSERT_EQ(status.code(), draco::Status::DRACO_ERROR); + ASSERT_EQ(status.code_string(), "DRACO_ERROR"); std::stringstream str; str << status; ASSERT_EQ(str.str(), "Error msg."); + + const draco::Status status2 = draco::ErrorStatus("Error msg2."); + ASSERT_EQ(status2.code(), draco::Status::DRACO_ERROR); + ASSERT_EQ(status2.error_msg_string(), "Error msg2."); + ASSERT_EQ(status2.code_string(), "DRACO_ERROR"); + ASSERT_EQ(status2.code_and_error_string(), "DRACO_ERROR: Error msg2."); } } // namespace diff --git a/contrib/draco/src/draco/core/vector_d.h b/contrib/draco/src/draco/core/vector_d.h index a3c46a46a..a0ec2dedf 100644 --- a/contrib/draco/src/draco/core/vector_d.h +++ b/contrib/draco/src/draco/core/vector_d.h @@ -34,7 +34,7 @@ class VectorD { typedef ScalarT Scalar; typedef VectorD Self; - // TODO(hemmer): Deprecate. + // TODO(b/199760123): Deprecate. typedef ScalarT CoefficientType; VectorD() { @@ -45,7 +45,7 @@ class VectorD { // The following constructor does not compile in opt mode, which for now led // to the constructors further down, which is not ideal. - // TODO(hemmer): fix constructor below and remove others. + // TODO(b/199760123): Fix constructor below and remove others. // template // explicit VectorD(Args... args) : v_({args...}) {} @@ -111,7 +111,7 @@ class VectorD { Scalar &operator[](int i) { return v_[i]; } const Scalar &operator[](int i) const { return v_[i]; } - // TODO(hemmer): remove. + // TODO(b/199760123): Remove. // Similar to interface of Eigen library. Scalar &operator()(int i) { return v_[i]; } const Scalar &operator()(int i) const { return v_[i]; } diff --git a/contrib/draco/src/draco/core/vector_d_test.cc b/contrib/draco/src/draco/core/vector_d_test.cc index d66128fb1..21c1ca4c5 100644 --- a/contrib/draco/src/draco/core/vector_d_test.cc +++ b/contrib/draco/src/draco/core/vector_d_test.cc @@ -32,16 +32,6 @@ typedef draco::Vector5ui Vector5ui; typedef draco::VectorD Vector3i; typedef draco::VectorD Vector4i; -template -void TestSquaredDistance(const draco::VectorD v1, - const draco::VectorD v2, - const CoeffT result) { - CoeffT squared_distance = SquaredDistance(v1, v2); - ASSERT_EQ(squared_distance, result); - squared_distance = SquaredDistance(v2, v1); - ASSERT_EQ(squared_distance, result); -} - TEST(VectorDTest, TestOperators) { { const Vector3f v; @@ -170,56 +160,6 @@ TEST(VectorTest, TestGetNormalizedWithZeroLengthVector) { ASSERT_EQ(normalized[2], 0); } -TEST(VectorDTest, TestSquaredDistance) { - // Test Vector2f: float, 2D. - Vector2f v1_2f(5.5, 10.5); - Vector2f v2_2f(3.5, 15.5); - float result_f = 29; - TestSquaredDistance(v1_2f, v2_2f, result_f); - - // Test Vector3f: float, 3D. - Vector3f v1_3f(5.5, 10.5, 2.3); - Vector3f v2_3f(3.5, 15.5, 0); - result_f = 34.29; - TestSquaredDistance(v1_3f, v2_3f, result_f); - - // Test Vector4f: float, 4D. - Vector4f v1_4f(5.5, 10.5, 2.3, 7.2); - Vector4f v2_4f(3.5, 15.5, 0, 9.9); - result_f = 41.58; - TestSquaredDistance(v1_4f, v2_4f, result_f); - - // Test Vector5f: float, 5D. - Vector5f v1_5f(5.5, 10.5, 2.3, 7.2, 1.0); - Vector5f v2_5f(3.5, 15.5, 0, 9.9, 0.2); - result_f = 42.22; - TestSquaredDistance(v1_5f, v2_5f, result_f); - - // Test Vector 2ui: uint32_t, 2D. - Vector2ui v1_2ui(5, 10); - Vector2ui v2_2ui(3, 15); - uint32_t result_ui = 29; - TestSquaredDistance(v1_2ui, v2_2ui, result_ui); - - // Test Vector 3ui: uint32_t, 3D. - Vector3ui v1_3ui(5, 10, 2); - Vector3ui v2_3ui(3, 15, 0); - result_ui = 33; - TestSquaredDistance(v1_3ui, v2_3ui, result_ui); - - // Test Vector 4ui: uint32_t, 4D. - Vector4ui v1_4ui(5, 10, 2, 7); - Vector4ui v2_4ui(3, 15, 0, 9); - result_ui = 37; - TestSquaredDistance(v1_4ui, v2_4ui, result_ui); - - // Test Vector 5ui: uint32_t, 5D. - Vector5ui v1_5ui(5, 10, 2, 7, 1); - Vector5ui v2_5ui(3, 15, 0, 9, 12); - result_ui = 158; - TestSquaredDistance(v1_5ui, v2_5ui, result_ui); -} - TEST(VectorDTest, TestCrossProduct3D) { const Vector3i e1(1, 0, 0); const Vector3i e2(0, 1, 0); diff --git a/contrib/draco/src/draco/io/file_reader_factory.cc b/contrib/draco/src/draco/io/file_reader_factory.cc index ac7b09288..a8f15a11f 100644 --- a/contrib/draco/src/draco/io/file_reader_factory.cc +++ b/contrib/draco/src/draco/io/file_reader_factory.cc @@ -1,5 +1,6 @@ #include "draco/io/file_reader_factory.h" +#include #include namespace draco { @@ -38,7 +39,6 @@ std::unique_ptr FileReaderFactory::OpenReader( } return reader; } - FILEREADER_LOG_ERROR("No file reader able to open input"); return nullptr; } diff --git a/contrib/draco/src/draco/io/file_utils.cc b/contrib/draco/src/draco/io/file_utils.cc index f93cbd899..694d259e8 100644 --- a/contrib/draco/src/draco/io/file_utils.cc +++ b/contrib/draco/src/draco/io/file_utils.cc @@ -14,6 +14,8 @@ // #include "draco/io/file_utils.h" +#include + #include "draco/io/file_reader_factory.h" #include "draco/io/file_reader_interface.h" #include "draco/io/file_writer_factory.h" @@ -30,7 +32,7 @@ void SplitPath(const std::string &full_path, std::string *out_folder_path, std::string ReplaceFileExtension(const std::string &in_file_name, const std::string &new_extension) { - const auto pos = in_file_name.find_last_of("."); + const auto pos = in_file_name.find_last_of('.'); if (pos == std::string::npos) { // No extension found. return in_file_name + "." + new_extension; @@ -46,6 +48,22 @@ std::string LowercaseFileExtension(const std::string &filename) { return parser::ToLower(filename.substr(pos + 1)); } +std::string LowercaseMimeTypeExtension(const std::string &mime_type) { + const size_t pos = mime_type.find_last_of('/'); + if (pos == 0 || pos == std::string::npos || pos == mime_type.length() - 1) { + return ""; + } + return parser::ToLower(mime_type.substr(pos + 1)); +} + +std::string RemoveFileExtension(const std::string &filename) { + const size_t pos = filename.find_last_of('.'); + if (pos == 0 || pos == std::string::npos || pos == filename.length() - 1) { + return filename; + } + return filename.substr(0, pos); +} + std::string GetFullPath(const std::string &input_file_relative_path, const std::string &sibling_file_full_path) { const auto pos = sibling_file_full_path.find_last_of("/\\"); @@ -76,6 +94,23 @@ bool ReadFileToBuffer(const std::string &file_name, return file_reader->ReadFileToBuffer(buffer); } +bool ReadFileToString(const std::string &file_name, std::string *contents) { + if (!contents) { + return false; + } + std::unique_ptr file_reader = + FileReaderFactory::OpenReader(file_name); + if (file_reader == nullptr) { + return false; + } + std::vector buffer; + if (!ReadFileToBuffer(file_name, &buffer)) { + return false; + } + contents->assign(buffer.begin(), buffer.end()); + return true; +} + bool WriteBufferToFile(const char *buffer, size_t buffer_size, const std::string &file_name) { std::unique_ptr file_writer = diff --git a/contrib/draco/src/draco/io/file_utils.h b/contrib/draco/src/draco/io/file_utils.h index 4b734e049..7a5fc475b 100644 --- a/contrib/draco/src/draco/io/file_utils.h +++ b/contrib/draco/src/draco/io/file_utils.h @@ -15,6 +15,7 @@ #ifndef DRACO_IO_FILE_UTILS_H_ #define DRACO_IO_FILE_UTILS_H_ +#include #include #include @@ -37,6 +38,13 @@ std::string ReplaceFileExtension(const std::string &in_file_name, // '.' (e.g. Linux hidden files), the first delimiter is ignored. std::string LowercaseFileExtension(const std::string &filename); +// Returns the mime type extension in lowercase if present, else "". Extension +// is defined as the string after the last '/ character. +std::string LowercaseMimeTypeExtension(const std::string &mime_type); + +// Returns the file name without extension. +std::string RemoveFileExtension(const std::string &filename); + // Given a path of the input file |input_file_relative_path| relative to the // parent directory of |sibling_file_full_path|, this function returns full path // to the input file. If |sibling_file_full_path| has no directory, the relative @@ -46,13 +54,18 @@ std::string LowercaseFileExtension(const std::string &filename); std::string GetFullPath(const std::string &input_file_relative_path, const std::string &sibling_file_full_path); -// Convenience method. Uses draco::FileReaderFactory internally. Reads contents +// Convenience methods. Uses draco::FileReaderFactory internally. Reads contents // of file referenced by |file_name| into |buffer| and returns true upon // success. bool ReadFileToBuffer(const std::string &file_name, std::vector *buffer); bool ReadFileToBuffer(const std::string &file_name, std::vector *buffer); +// Convenience method for reading a file into a std::string. Reads contents +// of file referenced by |file_name| into |contents| and returns true upon +// success. +bool ReadFileToString(const std::string &file_name, std::string *contents); + // Convenience method. Uses draco::FileWriterFactory internally. Writes contents // of |buffer| to file referred to by |file_name|. File is overwritten if it // exists. Returns true after successful write. diff --git a/contrib/draco/src/draco/io/file_utils_test.cc b/contrib/draco/src/draco/io/file_utils_test.cc index 4085ff0cd..b55b1e3be 100644 --- a/contrib/draco/src/draco/io/file_utils_test.cc +++ b/contrib/draco/src/draco/io/file_utils_test.cc @@ -14,6 +14,8 @@ // #include "draco/io/file_utils.h" +#include + #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" diff --git a/contrib/draco/src/draco/io/file_writer_factory.cc b/contrib/draco/src/draco/io/file_writer_factory.cc index cb6851602..8ffd5400a 100644 --- a/contrib/draco/src/draco/io/file_writer_factory.cc +++ b/contrib/draco/src/draco/io/file_writer_factory.cc @@ -1,5 +1,6 @@ #include "draco/io/file_writer_factory.h" +#include #include namespace draco { diff --git a/contrib/draco/src/draco/io/file_writer_utils.cc b/contrib/draco/src/draco/io/file_writer_utils.cc index bcadccfc6..3dab80bbf 100644 --- a/contrib/draco/src/draco/io/file_writer_utils.cc +++ b/contrib/draco/src/draco/io/file_writer_utils.cc @@ -7,6 +7,10 @@ #include "draco/draco_features.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "ghc/filesystem.hpp" +#endif // DRACO_TRANSCODER_SUPPORTED + namespace draco { void SplitPathPrivate(const std::string &full_path, @@ -30,8 +34,18 @@ void SplitPathPrivate(const std::string &full_path, } } -bool DirectoryExists(const std::string &path) { +bool DirectoryExists(const std::string &path_arg) { struct stat path_stat; + std::string path = path_arg; + +#if defined(_WIN32) && not defined(__MINGW32__) + // Avoid a silly windows issue: stat() will fail on a drive letter missing the + // trailing slash. + if (path.size() > 0 && path[path.size()] != '\\' && + path[path.size()] != '/') { + path.append("\\"); + } +#endif // Check if |path| exists. if (stat(path.c_str(), &path_stat) != 0) { @@ -50,7 +64,12 @@ bool CheckAndCreatePathForFile(const std::string &filename) { std::string basename; SplitPathPrivate(filename, &path, &basename); +#ifdef DRACO_TRANSCODER_SUPPORTED + const ghc::filesystem::path ghc_path(path); + ghc::filesystem::create_directories(ghc_path); +#endif // DRACO_TRANSCODER_SUPPORTED const bool directory_exists = DirectoryExists(path); + return directory_exists; } diff --git a/contrib/draco/src/draco/io/file_writer_utils_test.cc b/contrib/draco/src/draco/io/file_writer_utils_test.cc new file mode 100644 index 000000000..14a3013e0 --- /dev/null +++ b/contrib/draco/src/draco/io/file_writer_utils_test.cc @@ -0,0 +1,49 @@ +#include "draco/io/file_writer_utils.h" + +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace draco { +namespace { + +TEST(FileWriterUtilsTest, SplitPathPrivateNonWindows) { + const std::string test_path = "/path/to/file"; + std::string directory; + std::string file; + SplitPathPrivate(test_path, &directory, &file); + ASSERT_EQ(directory, "/path/to"); + ASSERT_EQ(file, "file"); +} + +TEST(FileWriterUtilsTest, SplitPathPrivateWindows) { + const std::string test_path = "C:\\path\\to\\file"; + std::string directory; + std::string file; + SplitPathPrivate(test_path, &directory, &file); + ASSERT_EQ(directory, "C:\\path\\to"); + ASSERT_EQ(file, "file"); +} + +TEST(FileWriterUtilsTest, DirectoryExistsTest) { + ASSERT_TRUE(DirectoryExists(GetTestTempDir())); + ASSERT_FALSE(DirectoryExists("fake/test/subdir")); +} + +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST(FileWriterUtilsTest, CheckAndCreatePathForFileTest) { + const std::string fake_file = "fake.file"; + const std::string fake_file_subdir = "a/few/dirs/down"; + const std::string test_temp_dir = GetTestTempDir(); + const std::string fake_file_directory = + test_temp_dir + "/" + fake_file_subdir; + const std::string fake_full_path = + test_temp_dir + "/" + fake_file_subdir + "/" + fake_file; + ASSERT_TRUE(CheckAndCreatePathForFile(fake_full_path)); + ASSERT_TRUE(DirectoryExists(fake_file_directory)); +} +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace +} // namespace draco diff --git a/contrib/draco/src/draco/io/gltf_decoder.cc b/contrib/draco/src/draco/io/gltf_decoder.cc new file mode 100644 index 000000000..521b7524f --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_decoder.cc @@ -0,0 +1,2893 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_decoder.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include +#include + +#include "draco/core/draco_types.h" +#include "draco/core/hash_utils.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/tiny_gltf_utils.h" +#include "draco/material/material_library.h" +#include "draco/mesh/mesh.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#include "draco/metadata/property_table.h" +#include "draco/point_cloud/point_cloud_builder.h" +#include "draco/scene/scene_indices.h" +#include "draco/texture/source_image.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +namespace { +draco::DataType GltfComponentTypeToDracoType(int component_type) { + switch (component_type) { + case TINYGLTF_COMPONENT_TYPE_BYTE: + return DT_INT8; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: + return DT_UINT8; + case TINYGLTF_COMPONENT_TYPE_SHORT: + return DT_INT16; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: + return DT_UINT16; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: + return DT_UINT32; + case TINYGLTF_COMPONENT_TYPE_FLOAT: + return DT_FLOAT32; + } + return DT_INVALID; +} + +GeometryAttribute::Type GltfAttributeToDracoAttribute( + const std::string attribute_name) { + if (attribute_name == "POSITION") { + return GeometryAttribute::POSITION; + } else if (attribute_name == "NORMAL") { + return GeometryAttribute::NORMAL; + } else if (attribute_name == "TEXCOORD_0") { + return GeometryAttribute::TEX_COORD; + } else if (attribute_name == "TEXCOORD_1") { + return GeometryAttribute::TEX_COORD; + } else if (attribute_name == "TANGENT") { + return GeometryAttribute::TANGENT; + } else if (attribute_name == "COLOR_0") { + return GeometryAttribute::COLOR; + } else if (attribute_name == "JOINTS_0") { + return GeometryAttribute::JOINTS; + } else if (attribute_name == "WEIGHTS_0") { + return GeometryAttribute::WEIGHTS; + } else if (attribute_name.rfind("_FEATURE_ID_") == 0) { + // Feature ID attribute like _FEATURE_ID_5 from EXT_mesh_features extension. + return GeometryAttribute::GENERIC; + } + return GeometryAttribute::INVALID; +} + +StatusOr TinyGltfToDracoAxisWrappingMode( + int wrap_mode) { + switch (wrap_mode) { + case TINYGLTF_TEXTURE_WRAP_CLAMP_TO_EDGE: + return TextureMap::CLAMP_TO_EDGE; + case TINYGLTF_TEXTURE_WRAP_MIRRORED_REPEAT: + return TextureMap::MIRRORED_REPEAT; + case TINYGLTF_TEXTURE_WRAP_REPEAT: + return TextureMap::REPEAT; + default: + return Status(Status::UNSUPPORTED_FEATURE, "Unsupported wrapping mode."); + } +} + +StatusOr TinyGltfToDracoFilterType(int filter_type) { + switch (filter_type) { + case -1: + return TextureMap::UNSPECIFIED; + case TINYGLTF_TEXTURE_FILTER_NEAREST: + return TextureMap::NEAREST; + case TINYGLTF_TEXTURE_FILTER_LINEAR: + return TextureMap::LINEAR; + case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_NEAREST: + return TextureMap::NEAREST_MIPMAP_NEAREST; + case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_NEAREST: + return TextureMap::LINEAR_MIPMAP_NEAREST; + case TINYGLTF_TEXTURE_FILTER_NEAREST_MIPMAP_LINEAR: + return TextureMap::NEAREST_MIPMAP_LINEAR; + case TINYGLTF_TEXTURE_FILTER_LINEAR_MIPMAP_LINEAR: + return TextureMap::LINEAR_MIPMAP_LINEAR; + default: + return Status(Status::DRACO_ERROR, "Unsupported texture filter type."); + } +} + +StatusOr> CopyDataAsUint32( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_BYTE) { + return Status(Status::DRACO_ERROR, "Byte cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_SHORT) { + return Status(Status::DRACO_ERROR, "Short cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_INT) { + return Status(Status::DRACO_ERROR, "Int cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT) { + return Status(Status::DRACO_ERROR, "Float cannot be converted to Uint32."); + } + if (accessor.componentType == TINYGLTF_COMPONENT_TYPE_DOUBLE) { + return Status(Status::DRACO_ERROR, "Double cannot be converted to Uint32."); + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, + "Error CopyDataAsUint32() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAsUint32() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const uint8_t *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + const int num_elements = accessor.count * num_components; + + std::vector output; + output.resize(num_elements); + + int out_index = 0; + const uint8_t *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + for (int c = 0; c < num_components; ++c) { + uint32_t value = 0; + memcpy(&value, data + (c * component_size), component_size); + output[out_index++] = value; + } + + data += byte_stride; + } + + return output; +} + +// Specialization for arithmetic types. +template < + typename TypeT, + typename std::enable_if::value>::type * = nullptr> +StatusOr> CopyDataAs(const tinygltf::Model &model, + const tinygltf::Accessor &accessor) { + if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Uint8."); + } + } else if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE != accessor.componentType && + TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Uint16."); + } + } else if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE != accessor.componentType && + TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT != accessor.componentType && + TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Uint32."); + } + } else if (std::is_same::value) { + if (TINYGLTF_COMPONENT_TYPE_FLOAT != accessor.componentType) { + return ErrorStatus("Accessor data cannot be converted to Float."); + } + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const uint8_t *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + + std::vector output; + output.resize(accessor.count); + + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + int out_index = 0; + const uint8_t *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + for (int c = 0; c < num_components; ++c) { + TypeT value = 0; + memcpy(&value, data + (c * component_size), component_size); + output[out_index++] = value; + } + data += byte_stride; + } + return output; +} + +// Specialization for remaining types is used for draco::VectorD. +template ::value>::type * = + nullptr> +StatusOr> CopyDataAs(const tinygltf::Model &model, + const tinygltf::Accessor &accessor) { + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + if (num_components != TypeT::dimension) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAs() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const uint8_t *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + + std::vector output; + output.resize(accessor.count); + + const uint8_t *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + TypeT values; + for (int c = 0; c < num_components; ++c) { + memcpy(&values[c], data + (c * component_size), component_size); + } + output[i] = values; + data += byte_stride; + } + return output; +} + +// Copies the data referenced from |buffer_view_id| into |data|. Currently only +// supports a byte stride of 0. I.e. tightly packed. +Status CopyDataFromBufferView(const tinygltf::Model &model, int buffer_view_id, + std::vector *data) { + if (buffer_view_id < 0) { + return ErrorStatus("Error CopyDataFromBufferView() bufferView < 0."); + } + const tinygltf::BufferView &buffer_view = model.bufferViews[buffer_view_id]; + if (buffer_view.buffer < 0) { + return ErrorStatus("Error CopyDataFromBufferView() buffer < 0."); + } + if (buffer_view.byteStride != 0) { + return Status(Status::DRACO_ERROR, "Error buffer view byteStride != 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + const uint8_t *const data_start = buffer.data.data() + buffer_view.byteOffset; + + data->resize(buffer_view.byteLength); + memcpy(&(*data)[0], data_start, buffer_view.byteLength); + return OkStatus(); +} + +// Returns a SourceImage created from |image|. +StatusOr> GetSourceImage( + const tinygltf::Model &model, const tinygltf::Image &image, + const Texture &texture) { + std::unique_ptr source_image(new SourceImage()); + // If the image is in an external file then the buffer view is < 0. + if (image.bufferView >= 0) { + DRACO_RETURN_IF_ERROR(CopyDataFromBufferView( + model, image.bufferView, &source_image->MutableEncodedData())); + } + source_image->set_filename(image.uri); + source_image->set_mime_type(image.mimeType); + + return source_image; +} + +std::unique_ptr GetNodeTrsMatrix(const tinygltf::Node &node) { + std::unique_ptr trsm(new TrsMatrix()); + if (node.matrix.size() == 16) { + Eigen::Matrix4d transformation; + // clang-format off + // |node.matrix| is in the column-major order. + transformation << + node.matrix[0], node.matrix[4], node.matrix[8], node.matrix[12], + node.matrix[1], node.matrix[5], node.matrix[9], node.matrix[13], + node.matrix[2], node.matrix[6], node.matrix[10], node.matrix[14], + node.matrix[3], node.matrix[7], node.matrix[11], node.matrix[15]; + // clang-format on + if (transformation != Eigen::Matrix4d::Identity()) { + trsm->SetMatrix(transformation); + } + } + + if (node.translation.size() == 3) { + const Eigen::Vector3d default_translation(0.0, 0.0, 0.0); + const Eigen::Vector3d node_translation( + node.translation[0], node.translation[1], node.translation[2]); + if (node_translation != default_translation) { + trsm->SetTranslation(node_translation); + } + } + if (node.scale.size() == 3) { + const Eigen::Vector3d default_scale(1.0, 1.0, 1.0); + const Eigen::Vector3d node_scale(node.scale[0], node.scale[1], + node.scale[2]); + if (node_scale != default_scale) { + trsm->SetScale(node_scale); + } + } + if (node.rotation.size() == 4) { + // Eigen quaternion is defined in (w, x, y, z) vs glTF that uses + // (x, y, z, w). + const Eigen::Quaterniond default_rotation(0.0, 0.0, 0.0, 1.0); + const Eigen::Quaterniond node_rotation(node.rotation[3], node.rotation[0], + node.rotation[1], node.rotation[2]); + if (node_rotation != default_rotation) { + trsm->SetRotation(node_rotation); + } + } + + return trsm; +} + +Eigen::Matrix4d UpdateMatrixForNormals( + const Eigen::Matrix4d &transform_matrix) { + Eigen::Matrix3d mat3x3; + // clang-format off + mat3x3 << + transform_matrix(0, 0), transform_matrix(0, 1), transform_matrix(0, 2), + transform_matrix(1, 0), transform_matrix(1, 1), transform_matrix(1, 2), + transform_matrix(2, 0), transform_matrix(2, 1), transform_matrix(2, 2); + // clang-format on + + mat3x3 = mat3x3.inverse().transpose(); + Eigen::Matrix4d mat4x4; + // clang-format off + mat4x4 << mat3x3(0, 0), mat3x3(0, 1), mat3x3(0, 2), 0.0, + mat3x3(1, 0), mat3x3(1, 1), mat3x3(1, 2), 0.0, + mat3x3(2, 0), mat3x3(2, 1), mat3x3(2, 2), 0.0, + 0.0, 0.0, 0.0, 1.0; + // clang-format on + return mat4x4; +} + +float Determinant(const Eigen::Matrix4d &transform_matrix) { + Eigen::Matrix3d mat3x3; + // clang-format off + mat3x3 << + transform_matrix(0, 0), transform_matrix(0, 1), transform_matrix(0, 2), + transform_matrix(1, 0), transform_matrix(1, 1), transform_matrix(1, 2), + transform_matrix(2, 0), transform_matrix(2, 1), transform_matrix(2, 2); + // clang-format on + return mat3x3.determinant(); +} + +bool FileExists(const std::string &filepath, void * /*user_data*/) { + return GetFileSize(filepath) != 0; +} + +bool ReadWholeFile(std::vector *out, std::string *err, + const std::string &filepath, void *user_data) { + if (!ReadFileToBuffer(filepath, out)) { + if (err) { + *err = "Unable to read: " + filepath; + } + return false; + } + if (user_data) { + auto *files_vector = + reinterpret_cast *>(user_data); + files_vector->push_back(filepath); + } + return true; +} + +bool WriteWholeFile(std::string * /*err*/, const std::string &filepath, + const std::vector &contents, + void * /*user_data*/) { + return WriteBufferToFile(contents.data(), contents.size(), filepath); +} + +} // namespace + +GltfDecoder::GltfDecoder() + : next_face_id_(0), + next_point_id_(0), + total_face_indices_count_(0), + total_point_indices_count_(0), + material_att_id_(-1) {} + +StatusOr> GltfDecoder::DecodeFromFile( + const std::string &file_name) { + return DecodeFromFile(file_name, nullptr); +} + +StatusOr> GltfDecoder::DecodeFromFile( + const std::string &file_name, std::vector *mesh_files) { + DRACO_RETURN_IF_ERROR(LoadFile(file_name, mesh_files)); + return BuildMesh(); +} + +StatusOr> GltfDecoder::DecodeFromBuffer( + DecoderBuffer *buffer) { + DRACO_RETURN_IF_ERROR(LoadBuffer(*buffer)); + return BuildMesh(); +} + +StatusOr> GltfDecoder::DecodeFromFileToScene( + const std::string &file_name) { + return DecodeFromFileToScene(file_name, nullptr); +} + +StatusOr> GltfDecoder::DecodeFromFileToScene( + const std::string &file_name, std::vector *scene_files) { + DRACO_RETURN_IF_ERROR(LoadFile(file_name, scene_files)); + scene_ = std::unique_ptr(new Scene()); + DRACO_RETURN_IF_ERROR(DecodeGltfToScene()); + return std::move(scene_); +} + +StatusOr> GltfDecoder::DecodeFromBufferToScene( + DecoderBuffer *buffer) { + DRACO_RETURN_IF_ERROR(LoadBuffer(*buffer)); + scene_ = std::unique_ptr(new Scene()); + DRACO_RETURN_IF_ERROR(DecodeGltfToScene()); + return std::move(scene_); +} + +Status GltfDecoder::LoadFile(const std::string &file_name, + std::vector *input_files) { + const std::string extension = LowercaseFileExtension(file_name); + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + const tinygltf::FsCallbacks fs_callbacks = { + &FileExists, + // TinyGLTF's ExpandFilePath does not do filesystem i/o, so it's safe to + // use in all environments. + &tinygltf::ExpandFilePath, &ReadWholeFile, &WriteWholeFile, + reinterpret_cast(input_files)}; + + loader.SetFsCallbacks(fs_callbacks); + + if (extension == "glb") { + if (!loader.LoadBinaryFromFile(&gltf_model_, &err, &warn, file_name)) { + return Status(Status::DRACO_ERROR, + "TinyGLTF failed to load glb file: " + err); + } + } else if (extension == "gltf") { + if (!loader.LoadASCIIFromFile(&gltf_model_, &err, &warn, file_name)) { + return Status(Status::DRACO_ERROR, + "TinyGLTF failed to load glTF file: " + err); + } + } else { + return Status(Status::DRACO_ERROR, "Unknown input file extension."); + } + DRACO_RETURN_IF_ERROR(CheckUnsupportedFeatures()); + input_file_name_ = file_name; + return OkStatus(); +} + +Status GltfDecoder::LoadBuffer(const DecoderBuffer &buffer) { + tinygltf::TinyGLTF loader; + std::string err; + std::string warn; + + if (!loader.LoadBinaryFromMemory( + &gltf_model_, &err, &warn, + reinterpret_cast(buffer.data_head()), + buffer.remaining_size())) { + return Status(Status::DRACO_ERROR, + "TinyGLTF failed to load glb buffer: " + err); + } + DRACO_RETURN_IF_ERROR(CheckUnsupportedFeatures()); + input_file_name_.clear(); + return OkStatus(); +} + +StatusOr> GltfDecoder::BuildMesh() { + DRACO_RETURN_IF_ERROR(GatherAttributeAndMaterialStats()); + if (total_face_indices_count_ > 0 && total_point_indices_count_ > 0) { + return ErrorStatus( + "Decoding to mesh can't handle triangle and point primitives at the " + "same time."); + } + if (total_face_indices_count_ > 0) { + mb_.Start(total_face_indices_count_ / 3); + DRACO_RETURN_IF_ERROR(AddAttributesToDracoMesh(&mb_)); + } else { + pb_.Start(total_point_indices_count_); + DRACO_RETURN_IF_ERROR(AddAttributesToDracoMesh(&pb_)); + } + + for (const tinygltf::Scene &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + const Eigen::Matrix4d parent_matrix = Eigen::Matrix4d::Identity(); + DRACO_RETURN_IF_ERROR(DecodeNode(scene.nodes[i], parent_matrix)); + } + } + DRACO_ASSIGN_OR_RETURN( + std::unique_ptr mesh, + BuildMeshFromBuilder(total_face_indices_count_ > 0, &mb_, &pb_)); + + DRACO_RETURN_IF_ERROR(CopyTextures(mesh.get())); + SetAttributePropertiesOnDracoMesh(mesh.get()); + DRACO_RETURN_IF_ERROR(AddMaterialsToDracoMesh(mesh.get())); + DRACO_RETURN_IF_ERROR(AddMeshFeaturesToDracoMesh(mesh.get())); + DRACO_RETURN_IF_ERROR(AddStructuralMetadataToGeometry(mesh.get())); + MoveNonMaterialTextures(mesh.get()); + return mesh; +} + +Status GltfDecoder::AddMeshFeaturesToDracoMesh(Mesh *mesh) { + for (const tinygltf::Scene &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + DRACO_RETURN_IF_ERROR(AddMeshFeaturesToDracoMesh(scene.nodes[i], mesh)); + } + } + return OkStatus(); +} + +Status GltfDecoder::AddMeshFeaturesToDracoMesh(int node_index, Mesh *mesh) { + const tinygltf::Node &node = gltf_model_.nodes[node_index]; + if (node.mesh >= 0) { + const tinygltf::Mesh &gltf_mesh = gltf_model_.meshes[node.mesh]; + for (const auto &primitive : gltf_mesh.primitives) { + // Decode mesh feature ID sets if present in this primitive. + DRACO_RETURN_IF_ERROR(DecodeMeshFeatures( + primitive, &mesh->GetMaterialLibrary().MutableTextureLibrary(), + mesh)); + } + } + for (int i = 0; i < node.children.size(); ++i) { + DRACO_RETURN_IF_ERROR(AddMeshFeaturesToDracoMesh(node.children[i], mesh)); + } + return OkStatus(); +} + +Status GltfDecoder::CheckUnsupportedFeatures() { + // Check for morph targets. + for (const auto &mesh : gltf_model_.meshes) { + for (const auto &primitive : mesh.primitives) { + if (!primitive.targets.empty()) { + return Status(Status::UNSUPPORTED_FEATURE, + "Morph targets are unsupported."); + } + } + } + + // Check for sparse accessors. + for (const auto &accessor : gltf_model_.accessors) { + if (accessor.sparse.isSparse) { + return Status(Status::UNSUPPORTED_FEATURE, + "Sparse accessors are unsupported."); + } + } + + // Check for extensions. + for (const auto &extension : gltf_model_.extensionsRequired) { + if (extension != "KHR_materials_unlit" && + extension != "KHR_texture_transform" && + extension != "KHR_draco_mesh_compression") { + return Status(Status::UNSUPPORTED_FEATURE, + extension + " is unsupported."); + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeNode(int node_index, + const Eigen::Matrix4d &parent_matrix) { + const tinygltf::Node &node = gltf_model_.nodes[node_index]; + const std::unique_ptr trsm = GetNodeTrsMatrix(node); + const Eigen::Matrix4d node_matrix = + parent_matrix * trsm->ComputeTransformationMatrix(); + + if (node.mesh >= 0) { + const tinygltf::Mesh &mesh = gltf_model_.meshes[node.mesh]; + for (const auto &primitive : mesh.primitives) { + DRACO_RETURN_IF_ERROR(DecodePrimitive(primitive, node_matrix)); + } + } + for (int i = 0; i < node.children.size(); ++i) { + DRACO_RETURN_IF_ERROR(DecodeNode(node.children[i], node_matrix)); + } + return OkStatus(); +} + +StatusOr GltfDecoder::DecodePrimitiveAttributeCount( + const tinygltf::Primitive &primitive) const { + // Use the first primitive attribute as all attributes have the same entry + // count according to glTF 2.0 spec. + if (primitive.attributes.empty()) { + return Status(Status::DRACO_ERROR, "Primitive has no attributes."); + } + const tinygltf::Accessor &accessor = + gltf_model_.accessors[primitive.attributes.begin()->second]; + return accessor.count; +} + +StatusOr GltfDecoder::DecodePrimitiveIndicesCount( + const tinygltf::Primitive &primitive) const { + if (primitive.indices < 0) { + // Primitive has implicit indices [0, 1, 2, 3, ...]. Determine indices count + // based on entry count of a primitive attribute. + return DecodePrimitiveAttributeCount(primitive); + } + const tinygltf::Accessor &indices = gltf_model_.accessors[primitive.indices]; + return indices.count; +} + +StatusOr> GltfDecoder::DecodePrimitiveIndices( + const tinygltf::Primitive &primitive) const { + std::vector indices_data; + if (primitive.indices < 0) { + // Primitive has implicit indices [0, 1, 2, 3, ...]. Create indices based on + // entry count of a primitive attribute. + DRACO_ASSIGN_OR_RETURN(const int num_vertices, + DecodePrimitiveAttributeCount(primitive)); + indices_data.reserve(num_vertices); + for (int i = 0; i < num_vertices; i++) { + indices_data.push_back(i); + } + } else { + // Get indices from the primitive's indices property. + const tinygltf::Accessor &indices = + gltf_model_.accessors[primitive.indices]; + if (indices.count <= 0) { + return Status(Status::DRACO_ERROR, "Could not convert indices."); + } + DRACO_ASSIGN_OR_RETURN(indices_data, + CopyDataAsUint32(gltf_model_, indices)); + } + return indices_data; +} + +Status GltfDecoder::DecodePrimitive(const tinygltf::Primitive &primitive, + const Eigen::Matrix4d &transform_matrix) { + if (primitive.mode != TINYGLTF_MODE_TRIANGLES && + primitive.mode != TINYGLTF_MODE_POINTS) { + return Status(Status::DRACO_ERROR, + "Primitive does not contain triangles or points."); + } + + // Store the transformation scale of this primitive loading as draco::Mesh. + if (scene_ == nullptr) { + // TODO(vytyaz): Do something for non-uniform scaling. + const float scale = transform_matrix.col(0).norm(); + gltf_primitive_material_to_scales_[primitive.material].push_back(scale); + } + + // Handle indices first. + DRACO_ASSIGN_OR_RETURN(const std::vector indices_data, + DecodePrimitiveIndices(primitive)); + const int number_of_faces = indices_data.size() / 3; + const int number_of_points = indices_data.size(); + + for (const auto &attribute : primitive.attributes) { + const tinygltf::Accessor &accessor = + gltf_model_.accessors[attribute.second]; + + const int att_id = + attribute_name_to_draco_mesh_attribute_id_[attribute.first]; + if (att_id == -1) { + continue; + } + + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_faces, + transform_matrix, &mb_)); + } else { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_points, + transform_matrix, &pb_)); + } + } + + // Add the material data only if there is more than one material. + if (gltf_primitive_material_to_draco_material_.size() > 1) { + const int material_index = primitive.material; + const auto it = + gltf_primitive_material_to_draco_material_.find(material_index); + if (it != gltf_primitive_material_to_draco_material_.end()) { + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_RETURN_IF_ERROR( + AddMaterialDataToBuilder(it->second, number_of_faces, &mb_)); + } else { + DRACO_RETURN_IF_ERROR( + AddMaterialDataToBuilder(it->second, number_of_points, &pb_)); + } + } + } + + next_face_id_ += number_of_faces; + next_point_id_ += number_of_points; + return OkStatus(); +} + +Status GltfDecoder::NodeGatherAttributeAndMaterialStats( + const tinygltf::Node &node) { + if (node.mesh >= 0) { + const tinygltf::Mesh &mesh = gltf_model_.meshes[node.mesh]; + for (const auto &primitive : mesh.primitives) { + DRACO_RETURN_IF_ERROR(AccumulatePrimitiveStats(primitive)); + + const auto it = + gltf_primitive_material_to_draco_material_.find(primitive.material); + if (it == gltf_primitive_material_to_draco_material_.end()) { + gltf_primitive_material_to_draco_material_[primitive.material] = + gltf_primitive_material_to_draco_material_.size(); + } + } + } + for (int i = 0; i < node.children.size(); ++i) { + const tinygltf::Node &child = gltf_model_.nodes[node.children[i]]; + DRACO_RETURN_IF_ERROR(NodeGatherAttributeAndMaterialStats(child)); + } + + return OkStatus(); +} + +Status GltfDecoder::GatherAttributeAndMaterialStats() { + for (const auto &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + const tinygltf::Node &node = gltf_model_.nodes[scene.nodes[i]]; + DRACO_RETURN_IF_ERROR(NodeGatherAttributeAndMaterialStats(node)); + } + } + return OkStatus(); +} + +void GltfDecoder::SumAttributeStats(const std::string &attribute_name, + int count) { + // We know that there must be a valid entry for |attribute_name| at this time. + mesh_attribute_data_[attribute_name].total_attribute_counts += count; +} + +Status GltfDecoder::CheckTypes(const std::string &attribute_name, + int component_type, int type, bool normalized) { + auto it_mad = mesh_attribute_data_.find(attribute_name); + + if (it_mad == mesh_attribute_data_.end()) { + MeshAttributeData mad; + mad.component_type = component_type; + mad.attribute_type = type; + mad.normalized = normalized; + mesh_attribute_data_[attribute_name] = mad; + return OkStatus(); + } + if (it_mad->second.component_type != component_type) { + return Status( + Status::DRACO_ERROR, + attribute_name + " attribute component type does not match previous."); + } + if (it_mad->second.attribute_type != type) { + return Status(Status::DRACO_ERROR, + attribute_name + " attribute type does not match previous."); + } + if (it_mad->second.normalized != normalized) { + return Status( + Status::DRACO_ERROR, + attribute_name + + " attribute normalized property does not match previous."); + } + + return OkStatus(); +} + +Status GltfDecoder::AccumulatePrimitiveStats( + const tinygltf::Primitive &primitive) { + DRACO_ASSIGN_OR_RETURN(const int indices_count, + DecodePrimitiveIndicesCount(primitive)); + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + total_face_indices_count_ += indices_count; + } else if (primitive.mode == TINYGLTF_MODE_POINTS) { + total_point_indices_count_ += indices_count; + } else { + return ErrorStatus("Unsupported primitive indices mode."); + } + + for (const auto &attribute : primitive.attributes) { + if (attribute.second >= gltf_model_.accessors.size()) { + return ErrorStatus("Invalid accessor."); + } + const tinygltf::Accessor &accessor = + gltf_model_.accessors[attribute.second]; + + DRACO_RETURN_IF_ERROR(CheckTypes(attribute.first, accessor.componentType, + accessor.type, accessor.normalized)); + SumAttributeStats(attribute.first, accessor.count); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddAttributesToDracoMesh(BuilderT *builder) { + for (const auto &attribute : mesh_attribute_data_) { + const GeometryAttribute::Type draco_att_type = + GltfAttributeToDracoAttribute(attribute.first); + if (draco_att_type == GeometryAttribute::INVALID) { + // Map an invalid attribute to attribute id -1 that will be ignored and + // not included in the Draco mesh. + attribute_name_to_draco_mesh_attribute_id_[attribute.first] = -1; + continue; + } + DRACO_ASSIGN_OR_RETURN( + const int att_id, + AddAttribute(draco_att_type, attribute.second.component_type, + attribute.second.attribute_type, builder)); + attribute_name_to_draco_mesh_attribute_id_[attribute.first] = att_id; + } + + // Add the material attribute. + if (gltf_model_.materials.size() > 1) { + draco::DataType component_type = DT_UINT32; + if (gltf_model_.materials.size() < 256) { + component_type = DT_UINT8; + } else if (gltf_model_.materials.size() < (1 << 16)) { + component_type = DT_UINT16; + } + material_att_id_ = + builder->AddAttribute(GeometryAttribute::MATERIAL, 1, component_type); + } + + return OkStatus(); +} + +template +Status GltfDecoder::AddAttributeValuesToBuilder( + const std::string &attribute_name, const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, const Eigen::Matrix4d &transform_matrix, + BuilderT *builder) { + const bool reverse_winding = Determinant(transform_matrix) < 0; + if (attribute_name == "TEXCOORD_0" || attribute_name == "TEXCOORD_1") { + DRACO_RETURN_IF_ERROR(AddTexCoordToBuilder(accessor, indices_data, att_id, + number_of_elements, + reverse_winding, builder)); + } else if (attribute_name == "TANGENT") { + const Eigen::Matrix4d matrix = UpdateMatrixForNormals(transform_matrix); + DRACO_RETURN_IF_ERROR(AddTangentToBuilder(accessor, indices_data, att_id, + number_of_elements, matrix, + reverse_winding, builder)); + } else if (attribute_name == "POSITION" || attribute_name == "NORMAL") { + const Eigen::Matrix4d matrix = + (attribute_name == "NORMAL") ? UpdateMatrixForNormals(transform_matrix) + : transform_matrix; + const bool normalize = (attribute_name == "NORMAL"); + DRACO_RETURN_IF_ERROR(AddTransformedDataToBuilder( + accessor, indices_data, att_id, number_of_elements, matrix, normalize, + reverse_winding, builder)); + } else if (attribute_name.rfind("_FEATURE_ID_") == 0) { + DRACO_RETURN_IF_ERROR(AddFeatureIdToBuilder( + accessor, indices_data, att_id, number_of_elements, reverse_winding, + attribute_name, builder)); + } else { + DRACO_RETURN_IF_ERROR(AddAttributeDataByTypes(accessor, indices_data, + att_id, number_of_elements, + reverse_winding, builder)); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddTangentToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, const Eigen::Matrix4d &transform_matrix, + bool reverse_winding, BuilderT *builder) { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + + for (int v = 0; v < data.size(); ++v) { + Eigen::Vector4d vec4(data[v][0], data[v][1], data[v][2], 1); + vec4 = transform_matrix * vec4; + + // Normalize the data. + Eigen::Vector3d vec3(vec4[0], vec4[1], vec4[2]); + vec3 = vec3.normalized(); + for (int i = 0; i < 3; ++i) { + vec4[i] = vec3[i]; + } + + // Add back the original w component. + vec4[3] = data[v][3]; + for (int i = 0; i < 4; ++i) { + data[v][i] = vec4[i]; + } + } + + SetValuesForBuilder(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); + return OkStatus(); +} + +template +Status GltfDecoder::AddTexCoordToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, bool reverse_winding, BuilderT *builder) { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + + // glTF stores texture coordinates flipped on the horizontal axis compared to + // how Draco stores texture coordinates. + for (auto &uv : data) { + uv[1] = 1.0 - uv[1]; + } + + SetValuesForBuilder(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); + return OkStatus(); +} + +template +Status GltfDecoder::AddFeatureIdToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, bool reverse_winding, + const std::string &attribute_name, BuilderT *builder) { + // Check that the feature ID attribute has correct type. + const int num_components = + TinyGltfUtils::GetNumComponentsForType(accessor.type); + if (num_components != 1) { + return ErrorStatus("Invalid feature ID attribute type."); + } + const draco::DataType draco_component_type = + GltfComponentTypeToDracoType(accessor.componentType); + if (draco_component_type != DT_UINT8 && draco_component_type != DT_UINT16 && + draco_component_type != DT_FLOAT32) { + return ErrorStatus("Invalid feature ID attribute component type."); + } + + // Set feature ID attribute values to mesh faces. + DRACO_RETURN_IF_ERROR(AddAttributeDataByTypes(accessor, indices_data, att_id, + number_of_elements, + reverse_winding, builder)); + + // Store feature ID attribute name with index like _FEATURE_ID_5 in Draco + // attribute metadata. + std::unique_ptr metadata(new draco::AttributeMetadata()); + metadata->AddEntryString("attribute_name", attribute_name); + builder->AddAttributeMetadata(att_id, std::move(metadata)); + return OkStatus(); +} + +template +Status GltfDecoder::AddTransformedDataToBuilder( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, const Eigen::Matrix4d &transform_matrix, + bool normalize, bool reverse_winding, BuilderT *builder) { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + + for (int v = 0; v < data.size(); ++v) { + Eigen::Vector4d vec4(data[v][0], data[v][1], data[v][2], 1); + vec4 = transform_matrix * vec4; + Eigen::Vector3d vec3(vec4[0], vec4[1], vec4[2]); + if (normalize) { + vec3 = vec3.normalized(); + } + for (int i = 0; i < 3; ++i) { + data[v][i] = vec3[i]; + } + } + + SetValuesForBuilder(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); + return OkStatus(); +} + +template +void GltfDecoder::SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, + bool reverse_winding, + TriangleSoupMeshBuilder *builder) { + SetValuesPerFace(indices_data, att_id, number_of_elements, data, + reverse_winding, builder); +} + +template +void GltfDecoder::SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, + bool reverse_winding, + PointCloudBuilder *builder) { + for (int i = 0; i < number_of_elements; ++i) { + const uint32_t v_id = indices_data[i]; + const PointIndex pi(v_id + next_point_id_); + builder->SetAttributeValueForPoint(att_id, pi, + GetDataContentAddress(data[v_id])); + } +} + +template +void GltfDecoder::SetValuesPerFace(const std::vector &indices_data, + int att_id, int number_of_faces, + const std::vector &data, + bool reverse_winding, + TriangleSoupMeshBuilder *mb) { + for (int f = 0; f < number_of_faces; ++f) { + const int base_corner = f * 3; + const uint32_t v_id = indices_data[base_corner]; + const int next_offset = reverse_winding ? 2 : 1; + const int prev_offset = reverse_winding ? 1 : 2; + const uint32_t v_next_id = indices_data[base_corner + next_offset]; + const uint32_t v_prev_id = indices_data[base_corner + prev_offset]; + + const FaceIndex face_index(f + next_face_id_); + mb->SetAttributeValuesForFace(att_id, face_index, + GetDataContentAddress(data[v_id]), + GetDataContentAddress(data[v_next_id]), + GetDataContentAddress(data[v_prev_id])); + } +} + +// Get the address of data content for arithmetic types |T|. +template +const void *GetDataContentAddressImpl(const T &data, + std::true_type /* is_arithmetic */) { + return &data; +} + +// Get the address of data content for vector types |T|. +template +const void *GetDataContentAddressImpl(const T &data, + std::false_type /* is_arithmetic */) { + return data.data(); +} + +template +const void *GltfDecoder::GetDataContentAddress(const T &data) const { + return GetDataContentAddressImpl(data, std::is_arithmetic()); +} + +template +Status GltfDecoder::AddAttributeDataByTypes( + const tinygltf::Accessor &accessor, + const std::vector &indices_data, int att_id, + int number_of_elements, bool reverse_winding, BuilderT *builder) { + typedef VectorD Vector2u8i; + typedef VectorD Vector3u8i; + typedef VectorD Vector4u8i; + typedef VectorD Vector2u16i; + typedef VectorD Vector3u16i; + typedef VectorD Vector4u16i; + switch (accessor.type) { + case TINYGLTF_TYPE_SCALAR: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, number_of_elements, + data, reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, number_of_elements, + data, reverse_winding, builder); + } break; + default: + return ErrorStatus("Add attribute data, unknown component type."); + } + break; + case TINYGLTF_TYPE_VEC2: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + default: + return Status(Status::DRACO_ERROR, + "Add attribute data, unknown component type."); + } + break; + case TINYGLTF_TYPE_VEC3: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + default: + return Status(Status::DRACO_ERROR, + "Add attribute data, unknown component type."); + } + break; + case TINYGLTF_TYPE_VEC4: + switch (accessor.componentType) { + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE: { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + CopyDataAs(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + case TINYGLTF_COMPONENT_TYPE_FLOAT: { + DRACO_ASSIGN_OR_RETURN( + std::vector data, + TinyGltfUtils::CopyDataAsFloat(gltf_model_, accessor)); + SetValuesForBuilder(indices_data, att_id, + number_of_elements, data, + reverse_winding, builder); + } break; + default: + return Status(Status::DRACO_ERROR, + "Add attribute data, unknown component type."); + } + break; + default: + return Status(Status::DRACO_ERROR, "Add attribute data, unknown type."); + } + return OkStatus(); +} + +template +Status GltfDecoder::CopyTextures(T *owner) { + for (int i = 0; i < gltf_model_.images.size(); ++i) { + const tinygltf::Image &image = gltf_model_.images[i]; + if (image.width == -1 || image.height == -1 || image.component == -1) { + // TinyGLTF does not return an error when it cannot find an image. It will + // add an image with negative values. + return Status(Status::DRACO_ERROR, "Error loading image."); + } + + std::unique_ptr draco_texture(new Texture()); + + // Update mapping between glTF images and textures in the texture library. + gltf_image_to_draco_texture_[i] = draco_texture.get(); + + DRACO_ASSIGN_OR_RETURN(std::unique_ptr source_image, + GetSourceImage(gltf_model_, image, *draco_texture)); + if (source_image->encoded_data().empty() && + !source_image->filename().empty()) { + // Update filename of source image to be relative of the glTF file. + std::string dirname; + std::string basename; + SplitPath(input_file_name_, &dirname, &basename); + source_image->set_filename(dirname + "/" + source_image->filename()); + } + draco_texture->set_source_image(*source_image); + + owner->GetMaterialLibrary().MutableTextureLibrary().PushTexture( + std::move(draco_texture)); + } + return OkStatus(); +} + +void GltfDecoder::SetAttributePropertiesOnDracoMesh(Mesh *mesh) { + for (const auto &mad : mesh_attribute_data_) { + const int att_id = attribute_name_to_draco_mesh_attribute_id_[mad.first]; + if (att_id == -1) { + continue; + } + if (mad.second.normalized) { + mesh->attribute(att_id)->set_normalized(true); + } + } +} + +Status GltfDecoder::AddMaterialsToDracoMesh(Mesh *mesh) { + bool is_normal_map_used = false; + + int default_material_index = -1; + const auto it = gltf_primitive_material_to_draco_material_.find(-1); + if (it != gltf_primitive_material_to_draco_material_.end()) { + default_material_index = it->second; + } + + int output_material_index = 0; + for (int input_material_index = 0; + input_material_index < gltf_model_.materials.size(); + ++input_material_index) { + if (default_material_index == input_material_index) { + // Insert a default material here for primitives that did not have a + // material index. + mesh->GetMaterialLibrary().MutableMaterial(output_material_index++); + } + + Material *const output_material = + mesh->GetMaterialLibrary().MutableMaterial(output_material_index++); + DRACO_RETURN_IF_ERROR( + AddGltfMaterial(input_material_index, output_material)); + if (output_material->GetTextureMapByType( + TextureMap::NORMAL_TANGENT_SPACE)) { + is_normal_map_used = true; + } + } + + return OkStatus(); +} + +template +Status GltfDecoder::AddMaterialDataToBuilder(int material_value, + int number_of_elements, + BuilderT *builder) { + if (gltf_primitive_material_to_draco_material_.size() < 256) { + const uint8_t typed_material_value = material_value; + DRACO_RETURN_IF_ERROR(AddMaterialDataToBuilderInternal( + typed_material_value, number_of_elements, builder)); + } else if (gltf_primitive_material_to_draco_material_.size() < (1 << 16)) { + const uint16_t typed_material_value = material_value; + DRACO_RETURN_IF_ERROR(AddMaterialDataToBuilderInternal( + typed_material_value, number_of_elements, builder)); + } else { + const uint32_t typed_material_value = material_value; + DRACO_RETURN_IF_ERROR(AddMaterialDataToBuilderInternal( + typed_material_value, number_of_elements, builder)); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddMaterialDataToBuilderInternal( + T material_value, int number_of_faces, TriangleSoupMeshBuilder *builder) { + for (int f = 0; f < number_of_faces; ++f) { + const FaceIndex face_index(f + next_face_id_); + builder->SetPerFaceAttributeValueForFace(material_att_id_, face_index, + &material_value); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddMaterialDataToBuilderInternal( + T material_value, int number_of_points, PointCloudBuilder *builder) { + for (int pi = 0; pi < number_of_points; ++pi) { + const PointIndex point_index(pi + next_point_id_); + builder->SetAttributeValueForPoint(material_att_id_, point_index, + &material_value); + } + return OkStatus(); +} + +Status GltfDecoder::CheckAndAddTextureToDracoMaterial( + int texture_index, int tex_coord_attribute_index, + const tinygltf::ExtensionMap &tex_info_ext, Material *material, + TextureMap::Type type) { + if (texture_index < 0) { + return OkStatus(); + } + + const tinygltf::Texture &input_texture = gltf_model_.textures[texture_index]; + int source_index = input_texture.source; + + const auto texture_it = gltf_image_to_draco_texture_.find(source_index); + if (texture_it != gltf_image_to_draco_texture_.end()) { + Texture *const texture = texture_it->second; + // Default GLTF 2.0 sampler uses REPEAT mode along both S and T directions. + TextureMap::WrappingMode wrapping_mode(TextureMap::REPEAT); + TextureMap::FilterType min_filter = TextureMap::UNSPECIFIED; + TextureMap::FilterType mag_filter = TextureMap::UNSPECIFIED; + + if (input_texture.sampler >= 0) { + const tinygltf::Sampler &sampler = + gltf_model_.samplers[input_texture.sampler]; + DRACO_ASSIGN_OR_RETURN(wrapping_mode.s, + TinyGltfToDracoAxisWrappingMode(sampler.wrapS)); + DRACO_ASSIGN_OR_RETURN(wrapping_mode.t, + TinyGltfToDracoAxisWrappingMode(sampler.wrapT)); + DRACO_ASSIGN_OR_RETURN(min_filter, + TinyGltfToDracoFilterType(sampler.minFilter)); + DRACO_ASSIGN_OR_RETURN(mag_filter, + TinyGltfToDracoFilterType(sampler.magFilter)); + } + if (tex_coord_attribute_index < 0 || tex_coord_attribute_index > 1) { + return Status(Status::DRACO_ERROR, "Incompatible tex coord index."); + } + TextureTransform transform; + DRACO_ASSIGN_OR_RETURN(const bool has_transform, + CheckKhrTextureTransform(tex_info_ext, &transform)); + if (has_transform) { + DRACO_RETURN_IF_ERROR(material->SetTextureMap( + texture, type, wrapping_mode, min_filter, mag_filter, transform, + tex_coord_attribute_index)); + } else { + DRACO_RETURN_IF_ERROR( + material->SetTextureMap(texture, type, wrapping_mode, min_filter, + mag_filter, tex_coord_attribute_index)); + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeGltfToScene() { + DRACO_RETURN_IF_ERROR(GatherAttributeAndMaterialStats()); + DRACO_RETURN_IF_ERROR(AddLightsToScene()); + DRACO_RETURN_IF_ERROR(AddMaterialsVariantsNamesToScene()); + DRACO_RETURN_IF_ERROR(AddStructuralMetadataToGeometry(scene_.get())); + DRACO_RETURN_IF_ERROR(CopyTextures(scene_.get())); + for (const tinygltf::Scene &scene : gltf_model_.scenes) { + for (int i = 0; i < scene.nodes.size(); ++i) { + DRACO_RETURN_IF_ERROR( + DecodeNodeForScene(scene.nodes[i], kInvalidSceneNodeIndex)); + scene_->AddRootNodeIndex(gltf_node_to_scenenode_index_[scene.nodes[i]]); + } + } + + DRACO_RETURN_IF_ERROR(AddAnimationsToScene()); + DRACO_RETURN_IF_ERROR(AddMaterialsToScene()); + DRACO_RETURN_IF_ERROR(AddSkinsToScene()); + MoveNonMaterialTextures(scene_.get()); + + return OkStatus(); +} + +Status GltfDecoder::AddLightsToScene() { + // Add all lights to Draco scene. + for (const auto &light : gltf_model_.lights) { + // Add a new light to the scene. + const LightIndex light_index = scene_->AddLight(); + Light *scene_light = scene_->GetLight(light_index); + + // Decode light type. + const std::map types = { + {"directional", Light::DIRECTIONAL}, + {"point", Light::POINT}, + {"spot", Light::SPOT}}; + if (types.count(light.type) == 0) { + return ErrorStatus("Light type is invalid."); + } + scene_light->SetType(types.at(light.type)); + + // Decode spot light properties. + if (scene_light->GetType() == Light::SPOT) { + scene_light->SetInnerConeAngle(light.spot.innerConeAngle); + scene_light->SetOuterConeAngle(light.spot.outerConeAngle); + } + + // Decode other light properties. + scene_light->SetName(light.name); + if (!light.color.empty()) { // Empty means that color is not specified. + if (light.color.size() != 3) { + return ErrorStatus("Light color is malformed."); + } + scene_light->SetColor( + Vector3f(light.color[0], light.color[1], light.color[2])); + } + scene_light->SetIntensity(light.intensity); + if (light.range != 0.0) { // Zero means that range is not specified. + if (light.range < 0.0) { + return ErrorStatus("Light range must be positive."); + } + scene_light->SetRange(light.range); + } + } + return OkStatus(); +} + +Status GltfDecoder::AddMaterialsVariantsNamesToScene() { + // Check whether the scene has materials variants. + const auto &e = gltf_model_.extensions.find("KHR_materials_variants"); + if (e == gltf_model_.extensions.end()) { + // The scene has no materials variants. + return OkStatus(); + } + + // Decode all materials variants names into Draco scene from JSON like this: + // "KHR_materials_variants": { + // "variants": [ + // {"name": "Loki" }, + // {"name": "Odin" }, + // ] + // } + const tinygltf::Value::Object &o = e->second.Get(); + const auto &variants = o.find("variants"); + if (variants == o.end()) { + return ErrorStatus("Materials variants extension with names is malformed."); + } + const tinygltf::Value &variants_array = variants->second; + if (!variants_array.IsArray()) { + return ErrorStatus("Materials variants names array is malformed."); + } + for (int i = 0; i < variants_array.Size(); i++) { + const auto &variant_object = variants_array.Get(i); + if (!variant_object.IsObject() || !variant_object.Has("name")) { + return ErrorStatus("Materials variants name is missing."); + } + const auto &name_string = variant_object.Get("name"); + if (!name_string.IsString()) { + return ErrorStatus("Materials variant name is malformed."); + } + const std::string &name = name_string.Get(); + scene_->GetMaterialLibrary().AddMaterialsVariant(name); + } + return OkStatus(); +} + +template +Status GltfDecoder::AddStructuralMetadataToGeometry(GeometryT *geometry) { + // Check whether the glTF model has structural metadata. + const auto &e = gltf_model_.extensions.find("EXT_structural_metadata"); + if (e == gltf_model_.extensions.end()) { + // The glTF model has no structural metadata. + return OkStatus(); + } + const tinygltf::Value::Object &o = e->second.Get(); + + // Decode property table schema. + { + const auto &value = o.find("schema"); + if (value == o.end()) { + return ErrorStatus("Structural metadata extension has no schema."); + } + const tinygltf::Value &object = value->second; + if (!object.IsObject()) { + return ErrorStatus("Structural metadata extension schema is malformed."); + } + + // Decodes tinygltf::Value into PropertyTable::Schema::Object. + struct SchemaParser { + static Status Parse(const tinygltf::Value &value, + PropertyTable::Schema::Object *object) { + switch (value.Type()) { + case tinygltf::OBJECT_TYPE: { + for (auto &it : value.Get()) { + object->SetObjects().emplace_back(it.first); + DRACO_RETURN_IF_ERROR( + Parse(it.second, &object->SetObjects().back())); + } + } break; + case tinygltf::ARRAY_TYPE: { + for (int i = 0; i < value.ArrayLen(); ++i) { + object->SetArray().emplace_back(); + DRACO_RETURN_IF_ERROR( + Parse(value.Get(i), &object->SetArray().back())); + } + } break; + case tinygltf::STRING_TYPE: + object->SetString(value.Get()); + break; + case tinygltf::INT_TYPE: + object->SetInteger(value.Get()); + break; + case tinygltf::BOOL_TYPE: + object->SetBoolean(value.Get()); + break; + case tinygltf::REAL_TYPE: + case tinygltf::BINARY_TYPE: + case tinygltf::NULL_TYPE: + default: + // Not used in the schema JSON. + return ErrorStatus("Unsupported JSON type in schema."); + } + return OkStatus(); + } + }; + + // Parse property table schema and set it to |geometry|. + PropertyTable::Schema schema; + DRACO_RETURN_IF_ERROR(SchemaParser::Parse(object, &schema.json)); + geometry->GetStructuralMetadata().SetPropertyTableSchema(schema); + } + + // Decode property tables. + { + const auto &tables = o.find("propertyTables"); + if (tables == o.end()) { + return ErrorStatus( + "Structural metadata extension has no property tables."); + } + const tinygltf::Value &tables_array = tables->second; + if (!tables_array.IsArray()) { + return ErrorStatus("Property tables array is malformed."); + } + + // Loop over all property tables. + for (int i = 0; i < tables_array.Size(); i++) { + // Create a property table and populate it below. + std::unique_ptr property_table(new PropertyTable()); + + const auto &object = tables_array.Get(i); + if (!object.IsObject()) { + return ErrorStatus("Property table is malformed."); + } + const auto o = object.Get(); + + // The "class" property is required. + bool success; + std::string str_value; + DRACO_ASSIGN_OR_RETURN(success, DecodeString("class", o, &str_value)); + if (success) { + property_table->SetClass(str_value); + } else { + return ErrorStatus("Property class is malformed."); + } + + // The "count" property is required. + int int_value; + DRACO_ASSIGN_OR_RETURN(success, DecodeInt("count", o, &int_value)); + if (success) { + property_table->SetCount(int_value); + } else { + return ErrorStatus("Property count is malformed."); + } + + // The "name" property is optional. + DRACO_ASSIGN_OR_RETURN(success, DecodeString("name", o, &str_value)); + if (success) { + property_table->SetName(str_value); + } + + // Decode property table properties (columns). + { + constexpr char kName[] = "properties"; + if (!object.Has(kName)) { + return ErrorStatus("Property table is malformed."); + } + const tinygltf::Value &value = object.Get(kName); + if (!value.IsObject()) { + return ErrorStatus( + "Property table properties property is malformed."); + } + + // Loop over property table properties. + for (const auto &key : value.Keys()) { + // Create a property table property and populate it below. + std::unique_ptr property( + new PropertyTable::Property()); + + const auto &property_object = value.Get(key); + if (!property_object.IsObject()) { + return ErrorStatus("Property entry is malformed."); + } + property->SetName(key); + const auto o = property_object.Get(); + + // The "values" property is required. + DRACO_ASSIGN_OR_RETURN( + success, + DecodePropertyTableData("values", o, &property->GetData())); + if (!success) { + return ErrorStatus("Property values property is malformed."); + } + + // All other properties are not required. + DRACO_ASSIGN_OR_RETURN( + success, DecodeString("stringOffsetType", o, &str_value)); + if (success) { + property->GetStringOffsets().type = str_value; + } + DRACO_ASSIGN_OR_RETURN( + success, DecodeString("arrayOffsetType", o, &str_value)); + if (success) { + property->GetArrayOffsets().type = str_value; + } + DRACO_ASSIGN_OR_RETURN( + success, + DecodePropertyTableData("arrayOffsets", o, + &property->GetArrayOffsets().data)); + DRACO_ASSIGN_OR_RETURN( + success, + DecodePropertyTableData("stringOffsets", o, + &property->GetStringOffsets().data)); + + // Add property to the property table. + property_table->AddProperty(std::move(property)); + } + } + + // Add property table to structural metadata. + geometry->GetStructuralMetadata().AddPropertyTable( + std::move(property_table)); + } + } + return OkStatus(); +} + +Status GltfDecoder::AddAnimationsToScene() { + for (const auto &animation : gltf_model_.animations) { + const AnimationIndex animation_index = scene_->AddAnimation(); + Animation *const encoder_animation = scene_->GetAnimation(animation_index); + encoder_animation->SetName(animation.name); + + for (const tinygltf::AnimationChannel &channel : animation.channels) { + const auto it = gltf_node_to_scenenode_index_.find(channel.target_node); + if (it == gltf_node_to_scenenode_index_.end()) { + return Status(Status::DRACO_ERROR, "Could not find Node in the scene."); + } + DRACO_RETURN_IF_ERROR(TinyGltfUtils::AddChannelToAnimation( + gltf_model_, animation, channel, it->second.value(), + encoder_animation)); + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeNodeForScene(int node_index, + SceneNodeIndex parent_index) { + SceneNodeIndex scene_node_index = kInvalidSceneNodeIndex; + SceneNode *scene_node = nullptr; + bool is_new_node; + if (gltf_scene_graph_mode_ == GltfSceneGraphMode::DAG && + gltf_node_to_scenenode_index_.find(node_index) != + gltf_node_to_scenenode_index_.end()) { + // Node has been decoded already. + scene_node_index = gltf_node_to_scenenode_index_[node_index]; + scene_node = scene_->GetNode(scene_node_index); + is_new_node = false; + } else { + scene_node_index = scene_->AddNode(); + // Update mapping between glTF Nodes and indices in the scene. + gltf_node_to_scenenode_index_[node_index] = scene_node_index; + + scene_node = scene_->GetNode(scene_node_index); + is_new_node = true; + } + + if (parent_index != kInvalidSceneNodeIndex) { + scene_node->AddParentIndex(parent_index); + SceneNode *const parent_node = scene_->GetNode(parent_index); + parent_node->AddChildIndex(scene_node_index); + } + + if (!is_new_node) { + return OkStatus(); + } + const tinygltf::Node &node = gltf_model_.nodes[node_index]; + if (!node.name.empty()) { + scene_node->SetName(node.name); + } + std::unique_ptr trsm = GetNodeTrsMatrix(node); + scene_node->SetTrsMatrix(*trsm); + if (node.skin >= 0) { + // Save the index to the source skins in the node. This will be updated + // later when the skins are processed. + scene_node->SetSkinIndex(SkinIndex(node.skin)); + } + if (node.mesh >= 0) { + // Check if we have already parsed this glTF Mesh. + const auto it = gltf_mesh_to_scene_mesh_group_.find(node.mesh); + if (it != gltf_mesh_to_scene_mesh_group_.end()) { + // We already processed this glTF mesh. + scene_node->SetMeshGroupIndex(it->second); + } else { + const MeshGroupIndex scene_mesh_group_index = scene_->AddMeshGroup(); + MeshGroup *const scene_mesh = + scene_->GetMeshGroup(scene_mesh_group_index); + + const tinygltf::Mesh &mesh = gltf_model_.meshes[node.mesh]; + if (!mesh.name.empty()) { + scene_mesh->SetName(mesh.name); + } + for (const auto &primitive : mesh.primitives) { + DRACO_RETURN_IF_ERROR(DecodePrimitiveForScene(primitive, scene_mesh)); + } + scene_node->SetMeshGroupIndex(scene_mesh_group_index); + gltf_mesh_to_scene_mesh_group_[node.mesh] = scene_mesh_group_index; + } + } + + // Decode light index. + const auto &e = node.extensions.find("KHR_lights_punctual"); + if (e != node.extensions.end()) { + const tinygltf::Value::Object &o = e->second.Get(); + const auto &light = o.find("light"); + if (light != o.end()) { + const tinygltf::Value &value = light->second; + if (!value.IsInt()) { + return ErrorStatus("Node light index is malformed."); + } + const int light_index = value.Get(); + if (light_index < 0 || light_index >= scene_->NumLights()) { + return ErrorStatus("Node light index is out of bounds."); + } + scene_node->SetLightIndex(LightIndex(light_index)); + } + } + + for (int i = 0; i < node.children.size(); ++i) { + DRACO_RETURN_IF_ERROR( + DecodeNodeForScene(node.children[i], scene_node_index)); + } + return OkStatus(); +} + +Status GltfDecoder::DecodePrimitiveForScene( + const tinygltf::Primitive &primitive, MeshGroup *mesh_group) { + if (primitive.mode != TINYGLTF_MODE_TRIANGLES && + primitive.mode != TINYGLTF_MODE_POINTS) { + return Status(Status::DRACO_ERROR, + "Primitive does not contain triangles or points."); + } + + // Decode materials variants mappings if present in this primitive. + std::vector mappings; + const auto &e = primitive.extensions.find("KHR_materials_variants"); + if (e != primitive.extensions.end()) { + DRACO_RETURN_IF_ERROR(DecodeMaterialsVariantsMappings( + e->second.Get(), &mappings)); + } + + const PrimitiveSignature signature(primitive); + const auto existing_mesh_index = + gltf_primitive_to_draco_mesh_index_.find(signature); + if (existing_mesh_index != gltf_primitive_to_draco_mesh_index_.end()) { + mesh_group->AddMeshInstance( + {existing_mesh_index->second, primitive.material, mappings}); + return OkStatus(); + } + + // Handle indices first. + DRACO_ASSIGN_OR_RETURN(const std::vector indices_data, + DecodePrimitiveIndices(primitive)); + const int number_of_faces = indices_data.size() / 3; + const int number_of_points = indices_data.size(); + + // Note that glTF mesh |primitive| has no name; no name is set to Draco mesh. + TriangleSoupMeshBuilder mb; + PointCloudBuilder pb; + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + mb.Start(number_of_faces); + } else { + pb.Start(number_of_points); + } + + std::set normalized_attributes; + for (const auto &attribute : primitive.attributes) { + if (attribute.second >= gltf_model_.accessors.size()) { + return ErrorStatus("Invalid accessor."); + } + const tinygltf::Accessor &accessor = + gltf_model_.accessors[attribute.second]; + const int component_type = accessor.componentType; + const int type = accessor.type; + const bool normalized = accessor.normalized; + int att_id = -1; + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_ASSIGN_OR_RETURN( + att_id, AddAttribute(attribute.first, component_type, type, &mb)); + } else { + DRACO_ASSIGN_OR_RETURN( + att_id, AddAttribute(attribute.first, component_type, type, &pb)); + } + if (att_id == -1) { + continue; + } + if (normalized) { + normalized_attributes.insert(att_id); + } + + if (primitive.mode == TINYGLTF_MODE_TRIANGLES) { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_faces, + Eigen::Matrix4d::Identity(), &mb)); + } else { + DRACO_RETURN_IF_ERROR(AddAttributeValuesToBuilder( + attribute.first, accessor, indices_data, att_id, number_of_points, + Eigen::Matrix4d::Identity(), &pb)); + } + } + + int material_index = primitive.material; + + DRACO_ASSIGN_OR_RETURN( + std::unique_ptr mesh, + BuildMeshFromBuilder(primitive.mode == TINYGLTF_MODE_TRIANGLES, &mb, + &pb)); + + // Set all normalized flags for appropriate attributes. + for (const int32_t att_id : normalized_attributes) { + mesh->attribute(att_id)->set_normalized(true); + } + // Decode mesh feature ID sets if present in this primitive. + DRACO_RETURN_IF_ERROR(DecodeMeshFeatures( + primitive, &scene_->GetMaterialLibrary().MutableTextureLibrary(), + mesh.get())); + + const MeshIndex mesh_index = scene_->AddMesh(std::move(mesh)); + if (mesh_index == kInvalidMeshIndex) { + return Status(Status::DRACO_ERROR, "Could not add Draco mesh to scene."); + } + mesh_group->AddMeshInstance({mesh_index, material_index, mappings}); + + gltf_primitive_to_draco_mesh_index_[signature] = mesh_index; + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialsVariantsMappings( + const tinygltf::Value::Object &extension, + std::vector *mappings) { + // Decode all materials variants mappings from JSON like this: + // "KHR_materials_variants" : { + // "mappings": [ + // { + // "material": 2, + // "variants": [0, 2, 4] + // }, + // { + // "material": 3, + // "variants": [1, 3] + // } + // ] + // } + const auto &mappings_object = extension.find("mappings"); + if (mappings_object == extension.end()) { + return ErrorStatus("Materials variants extension is malformed."); + } + const tinygltf::Value &mappings_array = mappings_object->second; + if (!mappings_array.IsArray()) { + return ErrorStatus("Materials variants mappings array is malformed."); + } + for (int i = 0; i < mappings_array.Size(); i++) { + const auto &mapping_object = mappings_array.Get(i); + if (!mapping_object.IsObject() || !mapping_object.Has("material") || + !mapping_object.Has("variants")) { + return ErrorStatus("Materials variants mapping is malformed."); + } + const tinygltf::Value &material_int = mapping_object.Get("material"); + if (!material_int.IsInt()) { + return ErrorStatus("Materials variant mapping material is malformed."); + } + const int material = material_int.Get(); + const tinygltf::Value &variants_array = mapping_object.Get("variants"); + if (!variants_array.IsArray()) { + return ErrorStatus("Materials variant mapping variants is malformed."); + } + std::vector variants; + for (int j = 0; j < variants_array.Size(); j++) { + const tinygltf::Value &variant_int = variants_array.Get(j); + if (!variant_int.IsInt()) { + return ErrorStatus("Materials variants mapping variant is malformed."); + } + variants.push_back(variant_int.Get()); + } + mappings->push_back({material, variants}); + } + return OkStatus(); +} + +Status GltfDecoder::DecodeMeshFeatures(const tinygltf::Primitive &primitive, + TextureLibrary *texture_library, + Mesh *mesh) { + const auto &e = primitive.extensions.find("EXT_mesh_features"); + if (e == primitive.extensions.end()) { + return OkStatus(); + } + std::vector> mesh_features; + DRACO_RETURN_IF_ERROR( + DecodeMeshFeatures(e->second.Get(), + texture_library, &mesh_features)); + for (int i = 0; i < mesh_features.size(); i++) { + const MeshFeaturesIndex mfi = + mesh->AddMeshFeatures(std::move(mesh_features[i])); + if (scene_ == nullptr) { + // If we are decoding to a mesh, we need to restrict the mesh features to + // the primitive's material. + // TODO(ostava): This will not work properly when two primitives share the + // same material but have different mesh features. We will need to + // duplicate the materials in this case. + const auto mat_it = + gltf_primitive_material_to_draco_material_.find(primitive.material); + if (mat_it != gltf_primitive_material_to_draco_material_.end()) { + mesh->AddMeshFeaturesMaterialMask(mfi, mat_it->second); + } + } + } + return OkStatus(); +} + +Status GltfDecoder::DecodeMeshFeatures( + const tinygltf::Value::Object &extension, TextureLibrary *texture_library, + std::vector> *mesh_features) { + // Decode all mesh feature ID sets from JSON like this: + // "EXT_mesh_features": { + // "featureIds": [ + // { + // "label": "water", + // "featureCount": 2, + // "propertyTable": 0, + // "attribute": 0 + // }, + // { + // "featureCount": 12, + // "nullFeatureId": 100, + // "texture" : { + // "index": 0, + // "texCoord": 0, + // "channels": [0, 1, 2, 3] + // } + // } + // ] + // } + const auto &object = extension.find("featureIds"); + if (object == extension.end()) { + return ErrorStatus("Mesh features extension is malformed."); + } + const tinygltf::Value &array = object->second; + if (!array.IsArray()) { + return ErrorStatus("Mesh features array is malformed."); + } + for (int i = 0; i < array.Size(); i++) { + // Create a new feature ID set object and populate it below. + mesh_features->push_back(std::unique_ptr(new MeshFeatures())); + MeshFeatures &features = *mesh_features->back(); + + const auto &object = array.Get(i); + if (!object.IsObject()) { + return ErrorStatus("Mesh features array entry is malformed."); + } + + // The "featureCount" property is required. + { + constexpr char kName[] = "featureCount"; + if (!object.Has(kName)) { + return ErrorStatus("Mesh features is malformed."); + } + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Feature count property is malformed."); + } + features.SetFeatureCount(value.Get()); + } + + // All other properties are optional. + { + constexpr char kName[] = "nullFeatureId"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Null feature ID property is malformed."); + } + features.SetNullFeatureId(value.Get()); + } + } + { + constexpr char kName[] = "label"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsString()) { + return ErrorStatus("Label property is malformed."); + } + features.SetLabel(value.Get()); + } + } + { + constexpr char kName[] = "attribute"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Attribute property is malformed."); + } + features.SetAttributeIndex(value.Get()); + } + } + { + constexpr char kName[] = "texture"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsObject()) { + return ErrorStatus("Texture property is malformed."); + } + + // Decode texture contining mesh feature IDs into the |features| object + // via a temporary |material| object. + Material material(texture_library); + const auto &container_object = object.Get(); + DRACO_RETURN_IF_ERROR(DecodeTexture(kName, TextureMap::GENERIC, + container_object, &material)); + features.SetTextureMap( + *material.GetTextureMapByType(TextureMap::GENERIC)); + + // Decode array of texture channel indices. + std::vector channels; + { + constexpr char kName[] = "channels"; + if (value.Has(kName)) { + const tinygltf::Value &array = value.Get(kName); + if (!array.IsArray()) { + return ErrorStatus("Channels property is malformed."); + } + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &value = array.Get(i); + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "Channels value is malformed."); + } + channels.push_back(value.Get()); + } + } else { + channels = {0}; + } + } + features.SetTextureChannels(channels); + } + } + { + constexpr char kName[] = "propertyTable"; + if (object.Has(kName)) { + const tinygltf::Value &value = object.Get(kName); + if (!value.IsInt()) { + return ErrorStatus("Property table property is malformed."); + } + features.SetPropertyTableIndex(value.Get()); + } + } + } + return OkStatus(); +} + +template +StatusOr GltfDecoder::AddAttribute(const std::string &attribute_name, + int component_type, int type, + BuilderT *builder) { + const GeometryAttribute::Type draco_att_type = + GltfAttributeToDracoAttribute(attribute_name); + if (draco_att_type == GeometryAttribute::INVALID) { + // Return attribute id -1 that will be ignored and not included in the mesh. + return -1; + } + DRACO_ASSIGN_OR_RETURN( + const int att_id, + AddAttribute(draco_att_type, component_type, type, builder)); + return att_id; +} + +template +StatusOr GltfDecoder::AddAttribute(GeometryAttribute::Type attribute_type, + int component_type, int type, + BuilderT *builder) { + const int num_components = TinyGltfUtils::GetNumComponentsForType(type); + if (num_components == 0) { + return Status(Status::DRACO_ERROR, + "Could not add attribute with 0 components."); + } + + const draco::DataType draco_component_type = + GltfComponentTypeToDracoType(component_type); + if (draco_component_type == DT_INVALID) { + return Status(Status::DRACO_ERROR, + "Could not add attribute with invalid type."); + } + const int att_id = builder->AddAttribute(attribute_type, num_components, + draco_component_type); + if (att_id < 0) { + return Status(Status::DRACO_ERROR, "Could not add attribute."); + } + return att_id; +} + +StatusOr GltfDecoder::CheckKhrTextureTransform( + const tinygltf::ExtensionMap &extension, TextureTransform *transform) { + bool transform_set = false; + + const auto &e = extension.find("KHR_texture_transform"); + if (e == extension.end()) { + return false; + } + const tinygltf::Value::Object &o = e->second.Get(); + const auto &scale = o.find("scale"); + if (scale != o.end()) { + const tinygltf::Value &array = scale->second; + if (!array.IsArray() || array.Size() != 2) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform scale is malformed."); + } + std::array scale; + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &value = array.Get(i); + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform scale is malformed."); + } + scale[i] = value.Get(); + transform_set = true; + } + transform->set_scale(scale); + } + const auto &rotation = o.find("rotation"); + if (rotation != o.end()) { + const tinygltf::Value &value = rotation->second; + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform rotation is malformed."); + } + transform->set_rotation(value.Get()); + transform_set = true; + } + const auto &offset = o.find("offset"); + if (offset != o.end()) { + const tinygltf::Value &array = offset->second; + if (!array.IsArray() || array.Size() != 2) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform offset is malformed."); + } + std::array offset; + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &value = array.Get(i); + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform offset is malformed."); + } + offset[i] = value.Get(); + transform_set = true; + } + transform->set_offset(offset); + } + const auto &tex_coord = o.find("texCoord"); + if (tex_coord != o.end()) { + const tinygltf::Value &value = tex_coord->second; + if (!value.IsInt()) { + return Status(Status::DRACO_ERROR, + "KhrTextureTransform texCoord is malformed."); + } + transform->set_tex_coord(value.Get()); + transform_set = true; + } + return transform_set; +} + +Status GltfDecoder::AddGltfMaterial(int input_material_index, + Material *output_material) { + const tinygltf::Material &input_material = + gltf_model_.materials[input_material_index]; + + output_material->SetName(input_material.name); + output_material->SetTransparencyMode( + TinyGltfUtils::TextToMaterialMode(input_material.alphaMode)); + output_material->SetAlphaCutoff(input_material.alphaCutoff); + if (input_material.emissiveFactor.size() == 3) { + output_material->SetEmissiveFactor(Vector3f( + input_material.emissiveFactor[0], input_material.emissiveFactor[1], + input_material.emissiveFactor[2])); + } + const tinygltf::PbrMetallicRoughness &pbr = + input_material.pbrMetallicRoughness; + + if (pbr.baseColorFactor.size() == 4) { + output_material->SetColorFactor( + Vector4f(pbr.baseColorFactor[0], pbr.baseColorFactor[1], + pbr.baseColorFactor[2], pbr.baseColorFactor[3])); + } + output_material->SetMetallicFactor(pbr.metallicFactor); + output_material->SetRoughnessFactor(pbr.roughnessFactor); + output_material->SetDoubleSided(input_material.doubleSided); + + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + pbr.baseColorTexture.index, pbr.baseColorTexture.texCoord, + pbr.baseColorTexture.extensions, output_material, TextureMap::COLOR)); + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + pbr.metallicRoughnessTexture.index, pbr.metallicRoughnessTexture.texCoord, + pbr.metallicRoughnessTexture.extensions, output_material, + TextureMap::METALLIC_ROUGHNESS)); + + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + input_material.normalTexture.index, input_material.normalTexture.texCoord, + input_material.normalTexture.extensions, output_material, + TextureMap::NORMAL_TANGENT_SPACE)); + if (input_material.normalTexture.scale != 1.0) { + output_material->SetNormalTextureScale(input_material.normalTexture.scale); + } + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + input_material.occlusionTexture.index, + input_material.occlusionTexture.texCoord, + input_material.occlusionTexture.extensions, output_material, + TextureMap::AMBIENT_OCCLUSION)); + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + input_material.emissiveTexture.index, + input_material.emissiveTexture.texCoord, + input_material.emissiveTexture.extensions, output_material, + TextureMap::EMISSIVE)); + + // Decode material extensions. + DecodeMaterialUnlitExtension(input_material, output_material); + DRACO_RETURN_IF_ERROR( + DecodeMaterialSheenExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialTransmissionExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialClearcoatExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR(DecodeMaterialVolumeExtension( + input_material, input_material_index, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialIorExtension(input_material, output_material)); + DRACO_RETURN_IF_ERROR( + DecodeMaterialSpecularExtension(input_material, output_material)); + + return OkStatus(); +} + +void GltfDecoder::DecodeMaterialUnlitExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_unlit"); + if (extension_it == input_material.extensions.end()) { + return; + } + + // Set the unlit property in Draco material. + output_material->SetUnlit(true); +} + +Status GltfDecoder::DecodeMaterialSheenExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_sheen"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasSheen(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode sheen color factor. + Vector3f vector; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeVector3f("sheenColorFactor", extension_object, &vector)); + if (success) { + output_material->SetSheenColorFactor(vector); + } + + // Decode sheen roughness factor. + float value; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("sheenRoughnessFactor", extension_object, &value)); + if (success) { + output_material->SetSheenRoughnessFactor(value); + } + + // Decode sheen color texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("sheenColorTexture", + TextureMap::SHEEN_COLOR, extension_object, + output_material)); + + // Decode sheen roughness texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("sheenRoughnessTexture", + TextureMap::SHEEN_ROUGHNESS, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialTransmissionExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_transmission"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasTransmission(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode transmission factor. + float value; + DRACO_ASSIGN_OR_RETURN( + const bool success, + DecodeFloat("transmissionFactor", extension_object, &value)); + if (success) { + output_material->SetTransmissionFactor(value); + } + + // Decode transmission texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("transmissionTexture", + TextureMap::TRANSMISSION, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialClearcoatExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_clearcoat"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasClearcoat(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode clearcoat factor. + float value; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("clearcoatFactor", extension_object, &value)); + if (success) { + output_material->SetClearcoatFactor(value); + } + + // Decode clearcoat roughness factor. + DRACO_ASSIGN_OR_RETURN(success, DecodeFloat("clearcoatRoughnessFactor", + extension_object, &value)); + if (success) { + output_material->SetClearcoatRoughnessFactor(value); + } + + // Decode clearcoat texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("clearcoatTexture", TextureMap::CLEARCOAT, + extension_object, output_material)); + + // Decode clearcoat roughness texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("clearcoatRoughnessTexture", + TextureMap::CLEARCOAT_ROUGHNESS, + extension_object, output_material)); + + // Decode clearcoat normal texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("clearcoatNormalTexture", + TextureMap::CLEARCOAT_NORMAL, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialVolumeExtension( + const tinygltf::Material &input_material, int input_material_index, + Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_volume"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasVolume(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode thickness factor. + float value; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("thicknessFactor", extension_object, &value)); + if (success) { + // Volume thickness factor is given in the coordinate space of the model. + // When the model is loaded as draco::Mesh, the scene graph transformations + // are applied to position attribute. Since this effectively scales the + // model coordinate space, the volume thickness factor also must be scaled. + // No scaling is done when the model is loaded as draco::Scene. + float scale = 1.0f; + if (scene_ == nullptr) { + if (gltf_primitive_material_to_scales_.count(input_material_index) == 1) { + const std::vector &scales = + gltf_primitive_material_to_scales_[input_material_index]; + + // It is only possible to scale the volume thickness factor if all + // primitives using this material have the same transformation scale. + // An alternative would be to create a separate meterial for each scale. + scale = scales[0]; + for (int i = 1; i < scales.size(); i++) { + // Note that close-enough scales could also be permitted. + if (scales[i] != scale) { + return ErrorStatus("Cannot represent volume thickness in a mesh."); + } + } + } + } + output_material->SetThicknessFactor(scale * value); + } + + // Decode attenuation distance. + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("attenuationDistance", extension_object, &value)); + if (success) { + output_material->SetAttenuationDistance(value); + } + + // Decode attenuation color. + Vector3f vector; + DRACO_ASSIGN_OR_RETURN( + success, DecodeVector3f("attenuationColor", extension_object, &vector)); + if (success) { + output_material->SetAttenuationColor(vector); + } + + // Decode thickness texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("thicknessTexture", TextureMap::THICKNESS, + extension_object, output_material)); + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialIorExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_ior"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasIor(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode index of refraction. + float value; + DRACO_ASSIGN_OR_RETURN(const bool success, + DecodeFloat("ior", extension_object, &value)); + if (success) { + output_material->SetIor(value); + } + + return OkStatus(); +} + +Status GltfDecoder::DecodeMaterialSpecularExtension( + const tinygltf::Material &input_material, Material *output_material) { + // Do nothing if extension is absent. + const auto &extension_it = + input_material.extensions.find("KHR_materials_specular"); + if (extension_it == input_material.extensions.end()) { + return OkStatus(); + } + + output_material->SetHasSpecular(true); + const tinygltf::Value::Object &extension_object = + extension_it->second.Get(); + + // Decode specular factor. + float value; + bool success; + DRACO_ASSIGN_OR_RETURN( + success, DecodeFloat("specularFactor", extension_object, &value)); + if (success) { + output_material->SetSpecularFactor(value); + } + + // Decode specular color factor. + Vector3f vector; + DRACO_ASSIGN_OR_RETURN(success, DecodeVector3f("specularColorFactor", + extension_object, &vector)); + if (success) { + output_material->SetSpecularColorFactor(vector); + } + + // Decode speclar texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("specularTexture", TextureMap::SPECULAR, + extension_object, output_material)); + + // Decode specular color texture. + DRACO_RETURN_IF_ERROR(DecodeTexture("specularColorTexture", + TextureMap::SPECULAR_COLOR, + extension_object, output_material)); + + return OkStatus(); +} + +StatusOr GltfDecoder::DecodeFloat(const std::string &name, + const tinygltf::Value::Object &object, + float *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &number = it->second; + if (!number.IsNumber()) { + return Status(Status::DRACO_ERROR, "Invalid " + name + "."); + } + *value = number.Get(); + return true; +} + +StatusOr GltfDecoder::DecodeInt(const std::string &name, + const tinygltf::Value::Object &object, + int *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &number = it->second; + if (!number.IsNumber()) { + return ErrorStatus("Invalid " + name + "."); + } + *value = number.Get(); + return true; +} + +StatusOr GltfDecoder::DecodeString(const std::string &name, + const tinygltf::Value::Object &object, + std::string *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &string = it->second; + if (!string.IsString()) { + return ErrorStatus("Invalid " + name + "."); + } + *value = string.Get(); + return true; +} + +StatusOr GltfDecoder::DecodePropertyTableData( + const std::string &name, const tinygltf::Value::Object &object, + PropertyTable::Property::Data *data) { + int buffer_view_index; + DRACO_ASSIGN_OR_RETURN(const bool success, + DecodeInt(name, object, &buffer_view_index)); + if (!success) { + return false; + } + DRACO_RETURN_IF_ERROR( + CopyDataFromBufferView(gltf_model_, buffer_view_index, &data->data)); + data->target = gltf_model_.bufferViews[buffer_view_index].target; + return true; +} + +StatusOr GltfDecoder::DecodeVector3f( + const std::string &name, const tinygltf::Value::Object &object, + Vector3f *value) { + const auto &it = object.find(name); + if (it == object.end()) { + return false; + } + const tinygltf::Value &array = it->second; + if (!array.IsArray() || array.Size() != 3) { + return Status(Status::DRACO_ERROR, "Invalid " + name + "."); + } + for (int i = 0; i < array.Size(); i++) { + const tinygltf::Value &array_entry = array.Get(i); + if (!array_entry.IsNumber()) { + return Status(Status::DRACO_ERROR, "Invalid " + name + "."); + } + (*value)[i] = array_entry.Get(); + } + return true; +} + +Status GltfDecoder::DecodeTexture(const std::string &name, + TextureMap::Type type, + const tinygltf::Value::Object &object, + Material *material) { + tinygltf::TextureInfo info; + DRACO_RETURN_IF_ERROR(ParseTextureInfo(name, object, &info)); + DRACO_RETURN_IF_ERROR(CheckAndAddTextureToDracoMaterial( + info.index, info.texCoord, info.extensions, material, type)); + return OkStatus(); +} + +Status GltfDecoder::ParseTextureInfo( + const std::string &texture_name, + const tinygltf::Value::Object &container_object, + tinygltf::TextureInfo *texture_info) { + // Note that tinygltf only parses material textures and not material extension + // textures. This method mimics the behavior of tinygltf's private function + // ParseTextureInfo() in order for Draco to decode extension textures. + + // Do nothing if texture with such name is absent. + const auto &texture_object_it = container_object.find(texture_name); + if (texture_object_it == container_object.end()) { + return OkStatus(); + } + + const tinygltf::Value::Object &texture_object = + texture_object_it->second.Get(); + + // Decode texture index. + const auto &index_it = texture_object.find("index"); + if (index_it != texture_object.end()) { + const tinygltf::Value &value = index_it->second; + if (!value.IsNumber()) { + return Status(Status::DRACO_ERROR, "Invalid texture index."); + } + texture_info->index = value.Get(); + } + + // Decode texture coordinate index. + const auto &tex_coord_it = texture_object.find("texCoord"); + if (tex_coord_it != texture_object.end()) { + const tinygltf::Value &value = tex_coord_it->second; + if (!value.IsInt()) { + return Status(Status::DRACO_ERROR, "Invalid texture texCoord."); + } + texture_info->texCoord = value.Get(); + } + + // Decode texture extensions. + const auto &extensions_it = texture_object.find("extensions"); + if (extensions_it != texture_object.end()) { + const tinygltf::Value &extensions = extensions_it->second; + if (!extensions.IsObject()) { + return Status(Status::DRACO_ERROR, "Invalid extension."); + } + for (const std::string &key : extensions.Keys()) { + texture_info->extensions[key] = extensions.Get(key); + } + } + + // Decode texture extras. + const auto &extras_it = texture_object.find("extras"); + if (extras_it != texture_object.end()) { + texture_info->extras = extras_it->second; + } + + return OkStatus(); +} + +Status GltfDecoder::AddMaterialsToScene() { + for (int input_material_index = 0; + input_material_index < gltf_model_.materials.size(); + ++input_material_index) { + Material *const output_material = + scene_->GetMaterialLibrary().MutableMaterial(input_material_index); + DRACO_RETURN_IF_ERROR( + AddGltfMaterial(input_material_index, output_material)); + } + + // Check if we need to add a default material for primitives without an + // assigned material. + const int default_material_index = + scene_->GetMaterialLibrary().NumMaterials(); + bool default_material_needed = false; + for (MeshGroupIndex mgi(0); mgi < scene_->NumMeshGroups(); ++mgi) { + MeshGroup *const mg = scene_->GetMeshGroup(mgi); + for (int mi = 0; mi < mg->NumMeshInstances(); ++mi) { + MeshGroup::MeshInstance &mesh_instance = mg->GetMeshInstance(mi); + if (mesh_instance.material_index == -1) { + mesh_instance.material_index = default_material_index; + default_material_needed = true; + } + } + } + if (default_material_needed) { + // Create an empty default material (our defaults correspond to glTF + // defaults). + scene_->GetMaterialLibrary().MutableMaterial(default_material_index); + } + + std::unordered_set meshes_that_need_tangents; + // Check if we need to generate tangent space for any of the loaded meshes. + for (MeshGroupIndex mgi(0); mgi < scene_->NumMeshGroups(); ++mgi) { + const MeshGroup *const mg = scene_->GetMeshGroup(mgi); + for (int mi = 0; mi < mg->NumMeshInstances(); ++mi) { + const MeshGroup::MeshInstance &mesh_instance = mg->GetMeshInstance(mi); + const auto tangent_map = + scene_->GetMaterialLibrary() + .GetMaterial(mesh_instance.material_index) + ->GetTextureMapByType(TextureMap::NORMAL_TANGENT_SPACE); + if (tangent_map != nullptr) { + Mesh &mesh = scene_->GetMesh(mesh_instance.mesh_index); + if (mesh.GetNamedAttribute(GeometryAttribute::TANGENT) == nullptr) { + meshes_that_need_tangents.insert(&mesh); + } + } + } + } + + return OkStatus(); +} + +Status GltfDecoder::AddSkinsToScene() { + for (int source_skin_index = 0; source_skin_index < gltf_model_.skins.size(); + ++source_skin_index) { + const tinygltf::Skin &skin = gltf_model_.skins[source_skin_index]; + const SkinIndex skin_index = scene_->AddSkin(); + Skin *const new_skin = scene_->GetSkin(skin_index); + + // The skin index was set previously while processing the nodes. + if (skin_index.value() != source_skin_index) { + return Status(Status::DRACO_ERROR, "Skin indices are mismatched."); + } + + if (skin.inverseBindMatrices >= 0) { + const tinygltf::Accessor &accessor = + gltf_model_.accessors[skin.inverseBindMatrices]; + DRACO_RETURN_IF_ERROR(TinyGltfUtils::AddAccessorToAnimationData( + gltf_model_, accessor, &new_skin->GetInverseBindMatrices())); + } + + if (skin.skeleton >= 0) { + const auto it = gltf_node_to_scenenode_index_.find(skin.skeleton); + if (it == gltf_node_to_scenenode_index_.end()) { + // TODO(b/200317162): If skeleton is not found set the default. + return Status(Status::DRACO_ERROR, + "Could not find skeleton in the skin."); + } + new_skin->SetJointRoot(it->second); + } + + for (int joint : skin.joints) { + const auto it = gltf_node_to_scenenode_index_.find(joint); + if (it == gltf_node_to_scenenode_index_.end()) { + // TODO(b/200317162): If skeleton is not found set the default. + return Status(Status::DRACO_ERROR, + "Could not find skeleton in the skin."); + } + new_skin->AddJoint(it->second); + } + } + return OkStatus(); +} + +void GltfDecoder::MoveNonMaterialTextures(Mesh *mesh) { + std::unordered_set non_material_textures; + for (MeshFeaturesIndex i(0); i < mesh->NumMeshFeatures(); i++) { + Texture *const texture = mesh->GetMeshFeatures(i).GetTextureMap().texture(); + if (texture != nullptr) { + non_material_textures.insert(texture); + } + } + MoveNonMaterialTextures(non_material_textures, + &mesh->GetMaterialLibrary().MutableTextureLibrary(), + &mesh->GetNonMaterialTextureLibrary()); +} + +void GltfDecoder::MoveNonMaterialTextures(Scene *scene) { + std::unordered_set non_material_textures; + for (MeshIndex i(0); i < scene->NumMeshes(); i++) { + for (MeshFeaturesIndex j(0); j < scene->GetMesh(i).NumMeshFeatures(); j++) { + Texture *const texture = + scene->GetMesh(i).GetMeshFeatures(j).GetTextureMap().texture(); + if (texture != nullptr) { + non_material_textures.insert(texture); + } + } + } + MoveNonMaterialTextures(non_material_textures, + &scene->GetMaterialLibrary().MutableTextureLibrary(), + &scene->GetNonMaterialTextureLibrary()); +} + +void GltfDecoder::MoveNonMaterialTextures( + const std::unordered_set &non_material_textures, + TextureLibrary *material_tl, TextureLibrary *non_material_tl) { + // TODO(vytyaz): Consider textures that are both material and non-material. + for (int i = 0; i < material_tl->NumTextures(); i++) { + // Move non-material texture from material to non-material texture library. + if (non_material_textures.count(material_tl->GetTexture(i)) == 1) { + non_material_tl->PushTexture(material_tl->RemoveTexture(i--)); + } + } +} + +bool GltfDecoder::PrimitiveSignature::operator==( + const PrimitiveSignature &signature) const { + return primitive.indices == signature.primitive.indices && + primitive.attributes == signature.primitive.attributes && + primitive.extras == signature.primitive.extras && + primitive.extensions == signature.primitive.extensions && + primitive.mode == signature.primitive.mode && + primitive.targets == signature.primitive.targets; +} + +size_t GltfDecoder::PrimitiveSignature::Hash::operator()( + const PrimitiveSignature &signature) const { + size_t hash = 79; // Magic number. + hash = HashCombine(signature.primitive.attributes.size(), hash); + for (auto it = signature.primitive.attributes.begin(); + it != signature.primitive.attributes.end(); ++it) { + hash = HashCombine(it->first, hash); + hash = HashCombine(it->second, hash); + } + hash = HashCombine(signature.primitive.indices, hash); + hash = HashCombine(signature.primitive.mode, hash); + return hash; +} + +StatusOr> GltfDecoder::BuildMeshFromBuilder( + bool use_mesh_builder, TriangleSoupMeshBuilder *mb, PointCloudBuilder *pb) { + std::unique_ptr mesh; + if (use_mesh_builder) { + mesh = mb->Finalize(); + } else { + std::unique_ptr pc = pb->Finalize(true); + if (pc) { + mesh.reset(new Mesh()); + PointCloud *mesh_pc = mesh.get(); + mesh_pc->Copy(*pc); + } + } + if (!mesh) { + return ErrorStatus("Failed to build Draco mesh from glTF data."); + } + return mesh; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_decoder.h b/contrib/draco/src/draco/io/gltf_decoder.h new file mode 100644 index 000000000..2ae12106e --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_decoder.h @@ -0,0 +1,524 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_GLTF_DECODER_H_ +#define DRACO_IO_GLTF_DECODER_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include + +#include "draco/core/decoder_buffer.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/tiny_gltf_utils.h" +#include "draco/mesh/mesh.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#include "draco/point_cloud/point_cloud_builder.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Decodes a glTF file and returns a draco::Mesh. All of the |mesh|'s attributes +// will be merged into one draco::Mesh +class GltfDecoder { + public: + GltfDecoder(); + + // Decodes a glTF file stored in the input |file_name| or |buffer| to a Mesh. + // The second form returns a vector of files used as input to the mesh during + // the decoding process. Returns nullptr when decode fails. + StatusOr> DecodeFromFile(const std::string &file_name); + StatusOr> DecodeFromFile( + const std::string &file_name, std::vector *mesh_files); + StatusOr> DecodeFromBuffer(DecoderBuffer *buffer); + + // Decodes a glTF file stored in the input |file_name| or |buffer| to a Scene. + // The second form returns a vector of files used as input to the scene during + // the decoding process. Returns nullptr if the decode fails. + StatusOr> DecodeFromFileToScene( + const std::string &file_name); + StatusOr> DecodeFromFileToScene( + const std::string &file_name, std::vector *scene_files); + StatusOr> DecodeFromBufferToScene( + DecoderBuffer *buffer); + + // Scene graph can be loaded either as a tree or a general directed acyclic + // graph (DAG) that allows multiple parent nodes. By default. we decode the + // scene graph as a tree. If the tree mode is selected and the input contains + // nodes with multiple parents, these nodes are duplicated to form a tree. + // TODO(ostava): Add support for DAG mode to other parts of the Draco + // library. + enum class GltfSceneGraphMode { TREE, DAG }; + void SetSceneGraphMode(GltfSceneGraphMode mode) { + gltf_scene_graph_mode_ = mode; + } + + private: + // Loads |file_name| into |gltf_model_|. Fills |input_files| with paths to all + // input files when non-null. + Status LoadFile(const std::string &file_name, + std::vector *input_files); + + // Loads |gltf_model_| from |buffer| in GLB format. + Status LoadBuffer(const DecoderBuffer &buffer); + + // Builds mesh from |gltf_model_|. + StatusOr> BuildMesh(); + + // Checks |gltf_model_| for unsupported features. If |gltf_model_| contains + // unsupported features then the function will return with a status code of + // UNSUPPORTED_FEATURE. + Status CheckUnsupportedFeatures(); + + // Decodes a glTF Node as well as any child Nodes. If |node| contains a mesh + // it will process all of the mesh's primitives. + Status DecodeNode(int node_index, const Eigen::Matrix4d &parent_matrix); + + // Decodes the number of entries in the first attribute of a given glTF + // |primitive|. Note that all attributes have the same entry count according + // to glTF 2.0 spec. + StatusOr DecodePrimitiveAttributeCount( + const tinygltf::Primitive &primitive) const; + + // Decodes the number of indices in a given glTF |primitive|. If primitive's + // indices property is not defined, the index count is implied from the entry + // count of a primitive attribute. + StatusOr DecodePrimitiveIndicesCount( + const tinygltf::Primitive &primitive) const; + + // Decodes indices property of a given glTF |primitive|. If primitive's + // indices property is not defined, the indices are generated based on entry + // count of a primitive attribute. + StatusOr> DecodePrimitiveIndices( + const tinygltf::Primitive &primitive) const; + + // Decodes a glTF Primitive. All of the |primitive|'s attributes will be + // merged into the draco::Mesh output if they are of the same type that + // already has been decoded. + Status DecodePrimitive(const tinygltf::Primitive &primitive, + const Eigen::Matrix4d &transform_matrix); + + // Sums the number of elements per attribute for |node|'s mesh and any of + // |node|'s children. Fills out the material index map. + Status NodeGatherAttributeAndMaterialStats(const tinygltf::Node &node); + + // Sums the number of elements per attribute for all of the meshes and + // primitives. + Status GatherAttributeAndMaterialStats(); + + // Sums the attribute counts into total_attribute_counts_. + void SumAttributeStats(const std::string &attribute_name, int count); + + // Checks that all the same glTF attribute types in different meshes and + // primitives contain the same characteristics. + Status CheckTypes(const std::string &attribute_name, int component_type, + int type, bool normalized); + + // Accumulates the number of elements per attribute for |primitive|. + Status AccumulatePrimitiveStats(const tinygltf::Primitive &primitive); + + // Adds all of the attributes from the glTF file to a Draco mesh. + // GatherAttributeAndMaterialStats() must be called before this function. The + // GeometryAttribute::MATERIAL attribute will be created only if the glTF file + // contains more than one material. + template + Status AddAttributesToDracoMesh(BuilderT *builder); + + // Copies attribute data from |accessor| and adds it to a Draco mesh using the + // geometry builder |builder|. + template + Status AddAttributeValuesToBuilder(const std::string &attribute_name, + const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + const Eigen::Matrix4d &transform_matrix, + BuilderT *builder); + + // Copies the tangent attribute data from |accessor| and adds it to a Draco + // mesh. This function will transform all of the data by |transform_matrix| + // and then normalize before adding the data to the Draco mesh. + // |indices_data| is the indices data from the glTF file. |att_id| is the + // attribute id of the tangent attribute in the Draco mesh. + // |number_of_elements| is the number of faces or points this function will + // process. |reverse_winding| if set will change the orientation of the data. + template + Status AddTangentToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + const Eigen::Matrix4d &transform_matrix, + bool reverse_winding, BuilderT *builder); + + // Copies the texture coordinate attribute data from |accessor| and adds it to + // a Draco mesh. This function will flip the data on the horizontal axis as + // Draco meshes store the texture coordinates differently than glTF. + // |indices_data| is the indices data from the glTF file. |att_id| is the + // attribute id of the texture coordinate attribute in the Draco mesh. + // |number_of_elements| is the number of faces or points this function will + // process. |reverse_winding| if set will change the orientation of the data. + template + Status AddTexCoordToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + bool reverse_winding, BuilderT *builder); + + // Copies the mesh feature ID attribute data from |accessor| and adds it to a + // Draco mesh. |indices_data| is the indices data from the glTF file. |att_id| + // is the attribute ID of the mesh feature ID attribute in the Draco mesh. + // |number_of_elements| is the number of faces or points this function will + // process. |reverse_winding| if set will change the orientation of the data. + template + Status AddFeatureIdToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + bool reverse_winding, + const std::string &attribute_name, + BuilderT *builder); + + // Copies the attribute data from |accessor| and adds it to a Draco mesh. + // This function will transform all of the data by |transform_matrix| before + // adding the data to the Draco mesh. |indices_data| is the indices data + // from the glTF file. |att_id| is the attribute id of the attribute in the + // Draco mesh. |number_of_elements| is the number of faces or points this + // function will process. |normalize| if set will normalize all of the vector + // data after transformation. |reverse_winding| if set will change the + // orientation of the data. + template + Status AddTransformedDataToBuilder(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + const Eigen::Matrix4d &transform_matrix, + bool normalize, bool reverse_winding, + BuilderT *builder); + + // Sets values in |data| into the builder |builder| for |att_id|. + template + void SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, bool reverse_winding, + TriangleSoupMeshBuilder *builder); + template + void SetValuesForBuilder(const std::vector &indices_data, + int att_id, int number_of_elements, + const std::vector &data, bool reverse_winding, + PointCloudBuilder *builder); + + // Sets values in |data| into the mesh builder |mb| for |att_id|. + // |reverse_winding| if set will change the orientation of the data. + template + void SetValuesPerFace(const std::vector &indices_data, int att_id, + int number_of_faces, const std::vector &data, + bool reverse_winding, TriangleSoupMeshBuilder *mb); + + // Returns an address pointing to the content stored in |data|. This is used + // when passing values to mesh / point cloud builder when the input type can + // be either a VectorD or an arithmetic type. + template + const void *GetDataContentAddress(const T &data) const; + + // Adds the attribute data in |accessor| to |mb| for unique attribute + // |att_id|. |indices_data| is the mesh's indices data. |reverse_winding| if + // set will change the orientation of the data. + template + Status AddAttributeDataByTypes(const tinygltf::Accessor &accessor, + const std::vector &indices_data, + int att_id, int number_of_elements, + bool reverse_winding, BuilderT *builder); + + // Adds the textures to |owner|. + template + Status CopyTextures(T *owner); + + // Sets extra attribute properties on a constructed draco mesh. + void SetAttributePropertiesOnDracoMesh(Mesh *mesh); + + // Adds the materials to |mesh|. + Status AddMaterialsToDracoMesh(Mesh *mesh); + + // Adds the material data for the GeometryAttribute::MATERIAL attribute to the + // Draco mesh. + template + Status AddMaterialDataToBuilder(int material_value, int number_of_elements, + BuilderT *builder); + template + Status AddMaterialDataToBuilderInternal(T material_value, int number_of_faces, + TriangleSoupMeshBuilder *builder); + template + Status AddMaterialDataToBuilderInternal(T material_value, + int number_of_points, + PointCloudBuilder *builder); + + // Checks if the glTF file contains a texture. If there is a texture, this + // function will read the texture data and add it to the Draco |material|. If + // there is no texture, this function will return OkStatus(). |texture_info| + // is the data structure containing information about the texture in the glTF + // file. |type| is the type of texture defined by Draco. This is not the same + // as the texture coordinate attribute id. + Status CheckAndAddTextureToDracoMaterial( + int texture_index, int tex_coord_attribute_index, + const tinygltf::ExtensionMap &tex_info_ext, Material *material, + TextureMap::Type type); + + // Decode glTF file to scene. + Status DecodeGltfToScene(); + + // Decode glTF lights into a scene. + Status AddLightsToScene(); + + // Decodes glTF materials variants names into a scene. + Status AddMaterialsVariantsNamesToScene(); + + // Decode glTF animations into a scene. All of the glTF nodes must be decoded + // to the scene before this function is called. + Status AddAnimationsToScene(); + + // Decode glTF node into a Draco scene. |parent_index| is the index of the + // parent node. If |node| is a root node set |parent_index| to + // |kInvalidSceneNodeIndex|. All glTF lights must be decoded to the scene + // before this function is called. + Status DecodeNodeForScene(int node_index, SceneNodeIndex parent_index); + + // Decode glTF primitive into a Draco scene. + Status DecodePrimitiveForScene(const tinygltf::Primitive &primitive, + MeshGroup *mesh_group); + + // Decodes glTF materials variants from |extension| and adds it into materials + // variants |mappings|. Before calling this function, all materials variants + // names must be decoded by calling AddMaterialsVariantsNamesToScene(). + Status DecodeMaterialsVariantsMappings( + const tinygltf::Value::Object &extension, + std::vector *mappings); + + // Decodes glTF mesh feature ID sets from all glTF primitives and adds them to + // |mesh|. + Status AddMeshFeaturesToDracoMesh(Mesh *mesh); + + // Decodes glTF mesh feature ID sets from glTF primitive in glTF node at + // |node_index| and adds them to |mesh|. + Status AddMeshFeaturesToDracoMesh(int node_index, Mesh *mesh); + + // Decodes glTF structural metadata from glTF model and adds it to |geometry|. + template + Status AddStructuralMetadataToGeometry(GeometryT *geometry); + + // Decodes glTF mesh feature ID sets from |primitive| and adds them to |mesh|. + Status DecodeMeshFeatures(const tinygltf::Primitive &primitive, + TextureLibrary *texture_library, Mesh *mesh); + + // Decodes glTF mesh feature ID sets from |extension| and adds them to the + // |mesh_features| vector. + Status DecodeMeshFeatures( + const tinygltf::Value::Object &extension, TextureLibrary *texture_library, + std::vector> *mesh_features); + + // Adds an attribute of type |attribute_name| to |builder|. Returns the + // attribute id. + template + StatusOr AddAttribute(const std::string &attribute_name, + int component_type, int type, BuilderT *builder); + + // Adds an attribute of |attribute_type| to |builder|. Returns the attribute + // id. + template + StatusOr AddAttribute(GeometryAttribute::Type attribute_type, + int component_type, int type, BuilderT *builder); + + // Returns true if the KHR_texture_transform extension is set in |extension|. + // If the KHR_texture_transform extension is set then the values are returned + // in |transform|. + StatusOr CheckKhrTextureTransform( + const tinygltf::ExtensionMap &extension, TextureTransform *transform); + + // Adds glTF material |input_material_index| to |output_material|. + Status AddGltfMaterial(int input_material_index, Material *output_material); + + // Adds unlit property from glTF |input_material| to |output_material|. + void DecodeMaterialUnlitExtension(const tinygltf::Material &input_material, + Material *output_material); + + // Adds sheen properties from glTF |input_material| to |output_material|. + Status DecodeMaterialSheenExtension(const tinygltf::Material &input_material, + Material *output_material); + + // Adds transmission from glTF |input_material| to |output_material|. + Status DecodeMaterialTransmissionExtension( + const tinygltf::Material &input_material, Material *output_material); + + // Adds clearcoat properties from glTF |input_material| to |output_material|. + Status DecodeMaterialClearcoatExtension( + const tinygltf::Material &input_material, Material *output_material); + + // Adds volume properties from glTF |input_material| to |output_material|. + Status DecodeMaterialVolumeExtension(const tinygltf::Material &input_material, + int input_material_index, + Material *output_material); + + // Adds ior properties from glTF |input_material| to |output_material|. + Status DecodeMaterialIorExtension(const tinygltf::Material &input_material, + Material *output_material); + + // Adds specular properties from glTF |input_material| to |output_material|. + Status DecodeMaterialSpecularExtension( + const tinygltf::Material &input_material, Material *output_material); + + // Decodes a float value with |name| from |object| to |value| and returns true + // if a well-formed value with such |name| is present. + static StatusOr DecodeFloat(const std::string &name, + const tinygltf::Value::Object &object, + float *value); + + // Decodes an integer value with |name| from |object| to |value| and returns + // true if a well-formed value with such |name| is present. + static StatusOr DecodeInt(const std::string &name, + const tinygltf::Value::Object &object, + int *value); + + // Decodes a string value with |name| from |object| to |value| and returns + // true if a well-formed value with such |name| is present. + static StatusOr DecodeString(const std::string &name, + const tinygltf::Value::Object &object, + std::string *value); + + // Decodes data and data target from buffer view index with |name| in |object| + // to |data| and returns true if a well-formed data is present. + StatusOr DecodePropertyTableData(const std::string &name, + const tinygltf::Value::Object &object, + PropertyTable::Property::Data *data); + + // Decodes a 3D vector with |name| from |object| to |value| and returns true + // if a well-formed vector with such |name| is present. + static StatusOr DecodeVector3f(const std::string &name, + const tinygltf::Value::Object &object, + Vector3f *value); + + // Decodes a texture with |name| from |object| and adds it to |material| as a + // texture map of |type|. + Status DecodeTexture(const std::string &name, TextureMap::Type type, + const tinygltf::Value::Object &object, + Material *material); + + // Reads texture with |texture_name| from |container_object| into + // |texture_info|. + static Status ParseTextureInfo( + const std::string &texture_name, + const tinygltf::Value::Object &container_object, + tinygltf::TextureInfo *texture_info); + + // Adds the materials to the scene. + Status AddMaterialsToScene(); + + // Adds the skins to the scene. + Status AddSkinsToScene(); + + // All material and non-material textures (e.g., from EXT_mesh_features) are + // initially loaded into a texture library inside the the material library. + // These methods move |non_material_textures| from material texture library + // |material_tl| to non-material texture library |non_material_tl|. + static void MoveNonMaterialTextures(Mesh *mesh); + static void MoveNonMaterialTextures(Scene *scene); + static void MoveNonMaterialTextures( + const std::unordered_set &non_material_textures, + TextureLibrary *material_tl, TextureLibrary *non_material_tl); + + // Builds and returns a mesh constructed from either mesh builder |mb| or + // point cloud builder |pb|. Mesh builder is used if |use_mesh_builder| is set + // to true. + static StatusOr> BuildMeshFromBuilder( + bool use_mesh_builder, TriangleSoupMeshBuilder *mb, + PointCloudBuilder *pb); + + // Map of glTF Mesh to Draco scene mesh group. + std::map gltf_mesh_to_scene_mesh_group_; + + // Data structure that stores the glTF data. + tinygltf::Model gltf_model_; + + // Path to the glTF file. + std::string input_file_name_; + + // Class used to build the Draco mesh. + TriangleSoupMeshBuilder mb_; + PointCloudBuilder pb_; + + // Next face index used when adding attribute data to the Draco mesh. + int next_face_id_; + + // Next point index used when adding attribute data to the point cloud. + int next_point_id_; + + // Total number of indices from all the meshes and primitives. + int total_face_indices_count_; + int total_point_indices_count_; + + // This is the id of the GeometryAttribute::MATERIAL attribute added to the + // Draco mesh. + int material_att_id_; + + // Data used when decoding the entire glTF asset into a single draco::Mesh. + // The struct tracks the total number of elements across all matching + // attributes and it ensures all matching attributes are compatible. + struct MeshAttributeData { + int component_type = 0; + int attribute_type = 0; + bool normalized = false; + int total_attribute_counts = 0; + }; + + // Map of glTF attribute name to attribute component type. + std::map mesh_attribute_data_; + + // Map of glTF attribute name to Draco mesh attribute id. + std::map attribute_name_to_draco_mesh_attribute_id_; + + // Map of glTF material to Draco material index. + std::map gltf_primitive_material_to_draco_material_; + + // Map of glTF material index to transformation scales of primitives. + std::map> gltf_primitive_material_to_scales_; + + // Map of glTF image to Draco textures. + std::map gltf_image_to_draco_texture_; + + std::unique_ptr scene_; + + // Map of glTF Node to local store order. + std::map gltf_node_to_scenenode_index_; + + // Selected mode of the decoded scene graph. + GltfSceneGraphMode gltf_scene_graph_mode_ = GltfSceneGraphMode::TREE; + + // Functionality for deduping primitives on decode. + struct PrimitiveSignature { + const tinygltf::Primitive &primitive; + explicit PrimitiveSignature(const tinygltf::Primitive &primitive) + : primitive(primitive) {} + bool operator==(const PrimitiveSignature &signature) const; + struct Hash { + size_t operator()(const PrimitiveSignature &signature) const; + }; + }; + std::unordered_map + gltf_primitive_to_draco_mesh_index_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_DECODER_H_ diff --git a/contrib/draco/src/draco/io/gltf_decoder_test.cc b/contrib/draco/src/draco/io/gltf_decoder_test.cc new file mode 100644 index 000000000..fade3ee26 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_decoder_test.cc @@ -0,0 +1,1402 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_decoder.h" + +#include +#include +#include +#include +#include +#include + +#include "draco/material/material_library.h" +#include "draco/scene/mesh_group.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/constants.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/core/draco_types.h" +#include "draco/io/gltf_test_helper.h" +#include "draco/io/texture_io.h" +#include "draco/mesh/mesh_are_equivalent.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/scene_indices.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +namespace { +std::unique_ptr DecodeGltfFile(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + GltfDecoder decoder; + + auto maybe_geometry = decoder.DecodeFromFile(path); + if (!maybe_geometry.ok()) { + return nullptr; + } + std::unique_ptr geometry = std::move(maybe_geometry).value(); + return geometry; +} + +std::unique_ptr DecodeGltfFileToScene(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + GltfDecoder decoder; + + auto maybe_scene = decoder.DecodeFromFileToScene(path); + if (!maybe_scene.ok()) { + return nullptr; + } + std::unique_ptr scene = std::move(maybe_scene).value(); + return scene; +} + +void CompareVectorArray(const std::array &a, + const std::array &b) { + for (int v = 0; v < 3; ++v) { + for (int c = 0; c < 3; ++c) { + EXPECT_FLOAT_EQ(a[v][c], b[v][c]) << "v:" << v << " c:" << c; + } + } +} +} // namespace + +// Tests multiple textures. +TEST(GltfDecoderTest, SphereGltf) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 231) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 224) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 2); +} + +TEST(GltfDecoderTest, TriangleGltf) { + const std::string file_name = "one_face_123.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + + const auto *const pos_attribute = + mesh->GetNamedAttribute(GeometryAttribute::POSITION); + EXPECT_NE(pos_attribute, nullptr); + const auto &face = mesh->face(FaceIndex(0)); + std::array pos; + for (int c = 0; c < 3; ++c) { + pos_attribute->GetMappedValue(face[c], &pos[c][0]); + } + + // Test position values match. + std::array pos_test; + pos_test[0] = Vector3f(1, 0.0999713, 0); + pos_test[1] = Vector3f(2.00006104, 0.01, 0); + pos_test[2] = Vector3f(3, 0.10998169, 0); + CompareVectorArray(pos, pos_test); +} + +TEST(GltfDecoderTest, MirroredTriangleGltf) { + const std::string file_name = "one_face_123_mirror.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + + const auto *const pos_attribute = + mesh->GetNamedAttribute(GeometryAttribute::POSITION); + EXPECT_NE(pos_attribute, nullptr); + const auto &face = mesh->face(FaceIndex(0)); + std::array pos; + for (int c = 0; c < 3; ++c) { + pos_attribute->GetMappedValue(face[c], &pos[c][0]); + } + + // Test position values match. + std::array pos_test; + pos_test[0] = Vector3f(-1, -0.0999713, 0); + pos_test[1] = Vector3f(-3, -0.10998169, 0); + pos_test[2] = Vector3f(-2.00006104, -0.01, 0); + CompareVectorArray(pos, pos_test); +} + +TEST(GltfDecoderTest, TranslateTriangleGltf) { + const std::string file_name = "one_face_123_translated.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + + const auto *const pos_attribute = + mesh->GetNamedAttribute(GeometryAttribute::POSITION); + EXPECT_NE(pos_attribute, nullptr); + const auto &face = mesh->face(FaceIndex(0)); + std::array pos; + for (int c = 0; c < 3; ++c) { + pos_attribute->GetMappedValue(face[c], &pos[c][0]); + } + + // Test position values match. The glTF file contains a matrix in the main + // node. The matrix defines a translation of (-1.5, 5.0, 2.3). + std::array pos_test; + pos_test[0] = Vector3f(1, 0.0999713, 0); + pos_test[1] = Vector3f(2.00006104, 0.01, 0); + pos_test[2] = Vector3f(3, 0.10998169, 0); + const Vector3f translate(-1.5, 5.0, 2.3); + for (int v = 0; v < 3; ++v) { + pos_test[v] = pos_test[v] + translate; + } + CompareVectorArray(pos, pos_test); +} + +// Tests multiple materials. +TEST(GltfDecoderTest, MilkTruckGltf) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3564) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 3624) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 4); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->NumTextureMaps(), 0); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(2)->NumTextureMaps(), 0); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(3)->NumTextureMaps(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->GetName(), "truck"); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->GetName(), "glass"); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(2)->GetName(), + "window_trim"); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(3)->GetName(), "wheels"); +} + +TEST(GltfDecoderTest, SceneMilkTruckGltf) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(scene->NumNodes(), 5); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->NumLights(), 0); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 4); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->NumTextureMaps(), 0); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(2)->NumTextureMaps(), 0); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(3)->NumTextureMaps(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetName(), "truck"); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->GetName(), "glass"); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(2)->GetName(), + "window_trim"); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(3)->GetName(), "wheels"); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 0); + for (AnimationIndex i(0); i < scene->NumAnimations(); ++i) { + const Animation *const animation = scene->GetAnimation(i); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 2); + ASSERT_EQ(animation->NumChannels(), 2); + } + + ASSERT_EQ(scene->GetMeshGroup(MeshGroupIndex(0))->GetName(), + "Cesium_Milk_Truck"); + ASSERT_EQ(scene->GetMeshGroup(MeshGroupIndex(1))->GetName(), "Wheels"); + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } +} + +TEST(GltfDecoderTest, AnimatedBonesGltf) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + ASSERT_EQ(scene->NumNodes(), 22); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 1); + for (AnimationIndex i(0); i < scene->NumAnimations(); ++i) { + const Animation *const animation = scene->GetAnimation(i); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 57); + ASSERT_EQ(animation->NumChannels(), 57); + } + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } +} + +TEST(GltfDecoderTest, AnimatedBonesGlb) { + const std::string file_name = "CesiumMan/glTF_Binary/CesiumMan.glb"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + ASSERT_EQ(scene->NumNodes(), 22); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 1); + for (AnimationIndex i(0); i < scene->NumAnimations(); ++i) { + const Animation *const animation = scene->GetAnimation(i); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 57); + ASSERT_EQ(animation->NumChannels(), 57); + } + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } +} + +// Tests multiple primitives with the same material index. +TEST(GltfDecoderTest, LanternGltf) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 4145) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 5394) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 4); +} + +// Tests COLOR_0 input attribute. +TEST(GltfDecoderTest, ColorAttributeGltf) { + const std::string file_name = "test_pos_color.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->num_attributes(), 2) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 114) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 224) << "Unexpected number of faces."; + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + ASSERT_NE(mesh->GetNamedAttribute(GeometryAttribute::COLOR), nullptr); + ASSERT_EQ(mesh->GetNamedAttribute(GeometryAttribute::COLOR)->data_type(), + draco::DT_UINT8); + // Ensure the normalized property for the color attribute is set properly. + ASSERT_TRUE(mesh->GetNamedAttribute(GeometryAttribute::COLOR)->normalized()); +} + +// Tests COLOR_0 input attribute when the asset is loaded into a scene. +TEST(GltfDecoderTest, ColorAttributeGltfScene) { + const std::string file_name = "test_pos_color.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_EQ(scene->NumMeshes(), 1); + const Mesh &mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_NE(mesh.GetNamedAttribute(GeometryAttribute::COLOR), nullptr); + ASSERT_EQ(mesh.GetNamedAttribute(GeometryAttribute::COLOR)->data_type(), + draco::DT_UINT8); + // Ensure the normalized property for the color attribute is set properly. + ASSERT_TRUE(mesh.GetNamedAttribute(GeometryAttribute::COLOR)->normalized()); +} + +// Tests a mesh with two sets of texture coordinates. +TEST(GltfDecoderTest, TwoTexCoordAttributesGltf) { + const std::string file_name = "sphere_two_tex_coords.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); +} + +// Tests an input with a valid tangent attribute does not auto generate the +// tangent attribute. +TEST(GltfDecoderTest, TestSceneWithTangents) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Ensure no mesh has auto-generated tangents (and that some meshes have the + // tangent attribute). + int num_tangent_attributes = 0; + for (MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + if (scene->GetMesh(mi).GetNamedAttribute(GeometryAttribute::TANGENT) != + nullptr) { + num_tangent_attributes++; + ASSERT_FALSE(MeshUtils::HasAutoGeneratedTangents(scene->GetMesh(mi))); + } + } + ASSERT_GT(num_tangent_attributes, 0); +} + +// Tests an input file where multiple textures share the same image asset. +TEST(GltfDecoderTest, SharedImages) { + const std::string file_name = "SphereAllSame/sphere_texture_all.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 5); + ASSERT_EQ(mesh->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 4); +} + +TEST(GltfDecoderTest, TextureNamesAreNotEmpty) { + const std::string file_name = "SphereAllSame/sphere_texture_all.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 5); + ASSERT_EQ(mesh->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 4); + const std::vector textures = { + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(0), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(1), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(2), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(3)}; + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[0]), "256x256_all_orange"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[1]), "256x256_all_blue"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[2]), "256x256_all_red"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[3]), "256x256_all_green"); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[0]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[1]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[2]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[3]), ImageFormat::PNG); +} + +TEST(GltfDecoderTest, TestTexCoord1) { + const std::string file_name = "MultiUVTest/glTF/MultiUVTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 2); + ASSERT_EQ(mesh->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 2); + const std::vector textures = { + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(0), + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(1)}; + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[0]), "uv0"); + EXPECT_EQ(TextureUtils::GetTargetStem(*textures[1]), "uv1"); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[0]), ImageFormat::PNG); + EXPECT_EQ(TextureUtils::GetTargetFormat(*textures[1]), ImageFormat::PNG); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::POSITION), 1); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::TANGENT), 1); +} + +TEST(GltfDecoderTest, SimpleScene) { + const std::string file_name = "Box/glTF/Box.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + ASSERT_EQ(scene->NumNodes(), 2); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 0); + ASSERT_EQ(scene->NumSkins(), 0); + ASSERT_EQ(scene->NumAnimations(), 0); + + // Check all of the meshes do not have any materials. + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + const Mesh &mesh = scene->GetMesh(i); + ASSERT_EQ(mesh.GetMaterialLibrary().NumMaterials(), 0); + } + + // Check names of nodes are empty. + EXPECT_TRUE(scene->GetNode(SceneNodeIndex(0))->GetName().empty()); + EXPECT_TRUE(scene->GetNode(SceneNodeIndex(1))->GetName().empty()); +} + +TEST(GltfDecoderTest, LanternScene) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + EXPECT_EQ(scene->NumMeshes(), 3); + EXPECT_EQ(scene->NumMeshGroups(), 3); + EXPECT_EQ(scene->NumNodes(), 4); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 4); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), + false); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->NumAnimations(), 0); + + // Check names of nodes have been populated. + EXPECT_EQ(scene->GetNode(SceneNodeIndex(0))->GetName(), "Lantern"); + EXPECT_EQ(scene->GetNode(SceneNodeIndex(1))->GetName(), "LanternPole_Body"); + EXPECT_EQ(scene->GetNode(SceneNodeIndex(2))->GetName(), "LanternPole_Chain"); + EXPECT_EQ(scene->GetNode(SceneNodeIndex(3))->GetName(), + "LanternPole_Lantern"); +} + +TEST(GltfDecoderTest, SimpleTriangleMesh) { + const std::string file_name = "Triangle/glTF/Triangle.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + + EXPECT_EQ(mesh->num_attributes(), 1) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 3) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 1) << "Unexpected number of faces."; + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 0); +} + +TEST(GltfDecoderTest, SimpleTriangleScene) { + const std::string file_name = "Triangle/glTF/Triangle.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + EXPECT_EQ(scene->NumNodes(), 1); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->NumAnimations(), 0); +} + +TEST(GltfDecoderTest, ThreeMeshesOneNoMaterialScene) { + const std::string file_name = + "three_meshes_two_materials_one_no_material/" + "three_meshes_two_materials_one_no_material.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + + EXPECT_EQ(scene->NumMeshes(), 3); + EXPECT_EQ(scene->NumMeshGroups(), 3); + EXPECT_EQ(scene->NumNodes(), 4); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 3); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->NumAnimations(), 0); +} + +TEST(GltfDecoderTest, ThreeMeshesOneNoMaterialMesh) { + const std::string file_name = + "three_meshes_two_materials_one_no_material/" + "three_meshes_two_materials_one_no_material.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + + EXPECT_EQ(mesh->num_attributes(), 4) << "Unexpected number of attributes."; + EXPECT_EQ(mesh->num_points(), 72) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 36) << "Unexpected number of faces."; + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 3); +} + +TEST(GltfDecoderTest, DoubleSidedMaterial) { + const std::string file_name = "TwoSidedPlane/glTF/TwoSidedPlane.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), true); + + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), true); +} + +TEST(GltfDecoderTest, VertexColorTest) { + const std::string file_name = "VertexColorTest/glTF/VertexColorTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(mesh->NumNamedAttributes(GeometryAttribute::COLOR), 1); + + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(scene->NumMeshes(), 2); + const Mesh &second_mesh = scene->GetMesh(MeshIndex(1)); + EXPECT_EQ(second_mesh.NumNamedAttributes(GeometryAttribute::COLOR), 1); +} + +TEST(GltfDecoderTest, MorphTargets) { + const std::string filename = + "KhronosSampleModels/AnimatedMorphCube/glTF/AnimatedMorphCube.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_FALSE(maybe_scene.ok()); + EXPECT_EQ(maybe_scene.status().code(), Status::Code::UNSUPPORTED_FEATURE); +} + +TEST(GltfDecoderTest, SparseAccessors) { + const std::string filename = + "KhronosSampleModels/SimpleSparseAccessor/glTF/SimpleSparseAccessor.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_FALSE(maybe_scene.ok()); + EXPECT_EQ(maybe_scene.status().code(), Status::Code::UNSUPPORTED_FEATURE); +} + +TEST(GltfDecoderTest, PbrSpecularGlossinessExtension) { + const std::string filename = + "KhronosSampleModels/SpecGlossVsMetalRough/glTF/" + "SpecGlossVsMetalRough.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_FALSE(maybe_scene.ok()); + EXPECT_EQ(maybe_scene.status().code(), Status::Code::UNSUPPORTED_FEATURE); +} + +TEST(GltfDecoderTest, DifferentWrappingModes) { + const std::string filename = + "KhronosSampleModels/TextureSettingsTest/glTF/TextureSettingsTest.gltf"; + const std::string path = GetTestFileFullPath(filename); + GltfDecoder decoder; + const auto maybe_scene = decoder.DecodeFromFileToScene(path); + EXPECT_TRUE(maybe_scene.ok()); + const draco::Scene &scene = *maybe_scene.value(); + ASSERT_EQ(scene.GetMaterialLibrary().GetTextureLibrary().NumTextures(), 3); + ASSERT_EQ(scene.GetMaterialLibrary().NumMaterials(), 10); + const draco::Material &material = *scene.GetMaterialLibrary().GetMaterial(0); + ASSERT_EQ(material.NumTextureMaps(), 1); + ASSERT_EQ(material.GetTextureMapByIndex(0)->wrapping_mode().s, + draco::TextureMap::REPEAT); + ASSERT_EQ(material.GetTextureMapByIndex(0)->wrapping_mode().t, + draco::TextureMap::MIRRORED_REPEAT); +} + +TEST(GltfDecoderTest, KhrMaterialsUnlitExtension) { + const std::string no_unlit_filename = "Box/glTF/Box.gltf"; + const std::unique_ptr scene_no_unlit( + DecodeGltfFileToScene(no_unlit_filename)); + EXPECT_EQ(scene_no_unlit->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene_no_unlit->GetMaterialLibrary().GetMaterial(0)->GetUnlit(), + false); + + const std::string filename = + "KhronosSampleModels/UnlitTest/glTF/UnlitTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(filename)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(mesh->GetMaterialLibrary().GetMaterial(0)->GetUnlit(), true); + EXPECT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->GetUnlit(), true); + + const std::unique_ptr scene(DecodeGltfFileToScene(filename)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 2); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetUnlit(), true); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->GetUnlit(), true); +} + +TEST(GltfDecoderTest, KhrMaterialsSheenExtension) { + // Check that a model with no sheen is loaded with no sheen. + { + const std::unique_ptr scene( + DecodeGltfFileToScene("Box/glTF/Box.gltf")); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + + // Check that material has no sheen. + const Material &material = *scene->GetMaterialLibrary().GetMaterial(0); + EXPECT_FALSE(material.HasSheen()); + + // Check that sheen color and roughness factors have default values. + EXPECT_EQ(material.GetSheenColorFactor(), Vector3f(0.f, 0.f, 0.f)); + EXPECT_EQ(material.GetSheenRoughnessFactor(), 0.f); + + // Check that sheen textures are absent. + EXPECT_EQ(material.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_EQ(material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), + nullptr); + } + + // Check that a model with sheen is loaded as a mesh with sheen. + { + // Load model as a mesh. + const std::unique_ptr mesh( + DecodeGltfFile("KhronosSampleModels/SheenCloth/glTF/SheenCloth.gltf")); + EXPECT_NE(mesh, nullptr); + const Material &material = *mesh->GetMaterialLibrary().GetMaterial(0); + + // Check that material has sheen. + EXPECT_TRUE(material.HasSheen()); + + // Check that sheen color and roughness factors are present. + EXPECT_EQ(material.GetSheenColorFactor(), Vector3f(1.f, 1.f, 1.f)); + EXPECT_EQ(material.GetSheenRoughnessFactor(), 1.f); + + // Check that sheen color and roughness textures are present. + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), + nullptr); + + // Check that sheen color and roughness textures are shared. + EXPECT_EQ( + material.GetTextureMapByType(TextureMap::SHEEN_COLOR)->texture(), + material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS)->texture()); + } + + // Check that a model with sheen is loaded as a scene with sheen. + { + // Load model as a scene. + const std::unique_ptr scene(DecodeGltfFileToScene( + "KhronosSampleModels/SheenCloth/glTF/SheenCloth.gltf")); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + const Material &material = *scene->GetMaterialLibrary().GetMaterial(0); + + // Check that material has sheen. + EXPECT_TRUE(material.HasSheen()); + + // Check that sheen color and roughness factors are present. + EXPECT_EQ(material.GetSheenColorFactor(), Vector3f(1.f, 1.f, 1.f)); + EXPECT_EQ(material.GetSheenRoughnessFactor(), 1.f); + + // Check that sheen color and roughness textures are present. + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_NE(material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), + nullptr); + + // Check that sheen color and roughness textures are shared. + EXPECT_EQ( + material.GetTextureMapByType(TextureMap::SHEEN_COLOR)->texture(), + material.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS)->texture()); + } +} + +TEST(GltfDecoderTest, PbrNextExtensions) { + // Check that a model with no material extensions is loaded correctly. + { + const std::unique_ptr scene( + DecodeGltfFileToScene("Box/glTF/Box.gltf")); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + const Material &m = *scene->GetMaterialLibrary().GetMaterial(0); + + // Check that material has no extensions. + EXPECT_FALSE(m.HasSheen()); + EXPECT_FALSE(m.HasTransmission()); + EXPECT_FALSE(m.HasClearcoat()); + EXPECT_FALSE(m.HasVolume()); + EXPECT_FALSE(m.HasIor()); + EXPECT_FALSE(m.HasSpecular()); + } + + // Check that a model with material extensions is loaded correctly. + { + const std::unique_ptr mesh( + DecodeGltfFile("pbr_next/sphere/glTF/sphere.gltf")); + EXPECT_NE(mesh, nullptr); + const Material &m = *mesh->GetMaterialLibrary().GetMaterial(0); + + // Check that material has extensions. + EXPECT_TRUE(m.HasSheen()); + EXPECT_TRUE(m.HasTransmission()); + EXPECT_TRUE(m.HasClearcoat()); + EXPECT_TRUE(m.HasVolume()); + EXPECT_TRUE(m.HasIor()); + EXPECT_TRUE(m.HasSpecular()); + + // Check that material has correct extension properties. + EXPECT_EQ(m.GetSheenColorFactor(), Vector3f(1.0f, 0.329f, 0.1f)); + EXPECT_EQ(m.GetSheenRoughnessFactor(), 0.8f); + EXPECT_EQ(m.GetTransmissionFactor(), 0.75f); + EXPECT_EQ(m.GetClearcoatFactor(), 0.95f); + EXPECT_EQ(m.GetClearcoatRoughnessFactor(), 0.03f); + EXPECT_EQ(m.GetAttenuationColor(), Vector3f(0.921f, 0.640f, 0.064f)); + EXPECT_EQ(m.GetAttenuationDistance(), 0.155f); + EXPECT_EQ(m.GetThicknessFactor(), 2.27f); + EXPECT_EQ(m.GetIor(), 1.55f); + EXPECT_EQ(m.GetSpecularFactor(), 0.3f); + EXPECT_EQ(m.GetSpecularColorFactor(), Vector3f(0.212f, 0.521f, 0.051f)); + + // Check that material has all extension textures. + EXPECT_NE(m.GetTextureMapByType(TextureMap::SHEEN_COLOR), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::SHEEN_ROUGHNESS), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::TRANSMISSION), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::CLEARCOAT), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::CLEARCOAT_ROUGHNESS), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::CLEARCOAT_NORMAL), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::THICKNESS), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::SPECULAR), nullptr); + EXPECT_NE(m.GetTextureMapByType(TextureMap::SPECULAR_COLOR), nullptr); + } +} + +TEST(GltfDecoderTest, TextureTransformTest) { + const std::string filename = + "KhronosSampleModels/TextureTransformTest/glTF/TextureTransformTest.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(filename)); + EXPECT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 9); + for (int i = 0; i < 6; ++i) { + EXPECT_FALSE(TextureTransform::IsDefault(mesh->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } + for (int i = 6; i < 9; ++i) { + EXPECT_TRUE(TextureTransform::IsDefault(mesh->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } + + const std::unique_ptr scene(DecodeGltfFileToScene(filename)); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 9); + for (int i = 0; i < 6; ++i) { + EXPECT_FALSE(TextureTransform::IsDefault(scene->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } + for (int i = 6; i < 9; ++i) { + EXPECT_TRUE(TextureTransform::IsDefault(scene->GetMaterialLibrary() + .GetMaterial(i) + ->GetTextureMapByIndex(0) + ->texture_transform())); + } +} + +TEST(GltfDecoderTest, GlbTextureSource) { + const std::string file_name = "KhronosSampleModels/Duck/glTF_Binary/Duck.glb"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + EXPECT_EQ(scene->NumNodes(), 3); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + EXPECT_EQ(scene->NumAnimations(), 0); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 1); + const Texture *const texture = + scene->GetMaterialLibrary().GetTextureLibrary().GetTexture(0); + ASSERT_NE(texture, nullptr); + const SourceImage &source_image = texture->source_image(); + EXPECT_EQ(source_image.encoded_data().size(), 16302); + EXPECT_EQ(source_image.filename(), ""); + EXPECT_EQ(source_image.mime_type(), "image/png"); +} + +TEST(GltfDecoderTest, GltfTextureSource) { + const std::string file_name = "KhronosSampleModels/Duck/glTF/Duck.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->NumMeshGroups(), 1); + const MeshGroup &mesh_group = *scene->GetMeshGroup(MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 1); + ASSERT_EQ(mesh_group.GetMeshInstance(0).material_index, 0); + EXPECT_EQ(scene->NumNodes(), 3); + EXPECT_EQ(scene->NumRootNodes(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 1); + EXPECT_EQ(scene->NumAnimations(), 0); + EXPECT_EQ(scene->NumSkins(), 0); + EXPECT_EQ(scene->GetMaterialLibrary().GetTextureLibrary().NumTextures(), 1); + const Texture *const texture = + scene->GetMaterialLibrary().GetTextureLibrary().GetTexture(0); + ASSERT_NE(texture, nullptr); + const SourceImage &source_image = texture->source_image(); + EXPECT_EQ(source_image.encoded_data().size(), 0); + EXPECT_FALSE(source_image.filename().empty()); + EXPECT_EQ(source_image.mime_type(), ""); +} + +TEST(GltfDecoderTest, GltfDecodeWithDraco) { + // Tests that we can decode a glTF containing Draco compressed geometry. + const std::string file_name = "Box/glTF_Binary/Box.glb"; + const std::string file_name_with_draco = "Box/glTF_Binary/Box_Draco.glb"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + const std::unique_ptr scene_draco( + DecodeGltfFileToScene(file_name_with_draco)); + ASSERT_NE(scene, nullptr); + ASSERT_NE(scene_draco, nullptr); + EXPECT_EQ(scene->NumMeshes(), scene_draco->NumMeshes()); + EXPECT_EQ(scene->NumMeshGroups(), scene_draco->NumMeshGroups()); + EXPECT_EQ(scene->NumNodes(), scene_draco->NumNodes()); + EXPECT_EQ(scene->NumRootNodes(), scene_draco->NumRootNodes()); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), + scene_draco->GetMaterialLibrary().NumMaterials()); + EXPECT_EQ(scene->NumAnimations(), scene_draco->NumAnimations()); + EXPECT_EQ(scene->NumSkins(), scene_draco->NumSkins()); + + EXPECT_EQ(scene->NumMeshes(), 1); + EXPECT_EQ(scene->GetMesh(draco::MeshIndex(0)).num_faces(), + scene_draco->GetMesh(draco::MeshIndex(0)).num_faces()); +} + +TEST(GltfDecoderTest, TestAnimationNames) { + const std::string file_name = "InterpolationTest/glTF/InterpolationTest.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + EXPECT_EQ(scene->NumAnimations(), 9); + + const std::vector animation_names{ + "Step Scale", "Linear Scale", + "CubicSpline Scale", "Step Rotation", + "CubicSpline Rotation", "Linear Rotation", + "Step Translation", "CubicSpline Translation", + "Linear Translation"}; + for (int i = 0; i < scene->NumAnimations(); ++i) { + const Animation *const anim = scene->GetAnimation(AnimationIndex(i)); + ASSERT_NE(anim, nullptr); + ASSERT_EQ(anim->GetName(), animation_names[i]); + } +} + +TEST(GltfDecoderTest, DuplicatePrimitives) { + const std::string file_name = "DuplicateMeshes/duplicate_meshes.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // There should be only one unique base mesh in the scene and four mesh + // groups (instances). + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 4); + + // There should be two materials used by the instances. + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 2); +} + +TEST(GltfDecoderTest, SimpleSkin) { + // This is a simple skin example from glTF tutorial. + const std::string file_name = "simple_skin.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Check scene size. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + ASSERT_EQ(scene->GetMeshGroup(draco::MeshGroupIndex(0))->NumMeshInstances(), + 1); + ASSERT_EQ(scene->NumNodes(), 3); + ASSERT_EQ(scene->NumRootNodes(), 1); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene->NumAnimations(), 1); + ASSERT_EQ(scene->NumSkins(), 1); + + // Check animation size. + const Animation *const animation = scene->GetAnimation(AnimationIndex(0)); + ASSERT_NE(animation, nullptr); + ASSERT_EQ(animation->NumSamplers(), 1); + ASSERT_EQ(animation->NumChannels(), 1); + ASSERT_EQ(animation->NumNodeAnimationData(), 2); + + // Check animation sampler. + const AnimationSampler *const sampler = animation->GetSampler(0); + ASSERT_NE(sampler, nullptr); + ASSERT_EQ(sampler->input_index, 0); + ASSERT_EQ(sampler->interpolation_type, + AnimationSampler::SamplerInterpolation::LINEAR); + ASSERT_EQ(sampler->output_index, 1); + + // Check animation channel. + const AnimationChannel *const channel = animation->GetChannel(0); + ASSERT_NE(channel, nullptr); + ASSERT_EQ(channel->sampler_index, 0); + ASSERT_EQ(channel->target_index, 2); + ASSERT_EQ(channel->transformation_type, + AnimationChannel::ChannelTransformation::ROTATION); + + // Check the first node animation data. + { + const NodeAnimationData *const node_animation = + animation->GetNodeAnimationData(0); + ASSERT_EQ(node_animation->ComponentSize(), 4); + ASSERT_EQ(node_animation->NumComponents(), 1); + ASSERT_EQ(node_animation->count(), 12); + ASSERT_EQ(node_animation->type(), NodeAnimationData::Type::SCALAR); + ASSERT_FALSE(node_animation->normalized()); + const std::vector &node_animation_data = *node_animation->GetData(); + const std::vector expected_node_animation_data{ + 0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f, 3.5f, 4.0f, 4.5f, 5.0f, 5.5f}; + ASSERT_EQ(node_animation_data, expected_node_animation_data); + } + + // Check the second node animation data. + { + const NodeAnimationData *const node_animation = + animation->GetNodeAnimationData(1); + ASSERT_EQ(node_animation->ComponentSize(), 4); + ASSERT_EQ(node_animation->NumComponents(), 4); + ASSERT_EQ(node_animation->count(), 12); + ASSERT_EQ(node_animation->type(), NodeAnimationData::Type::VEC4); + ASSERT_FALSE(node_animation->normalized()); + const std::vector &node_animation_data = *node_animation->GetData(); + std::cout << std::endl; + // clang-format off + const std::vector expected_node_animation_data{ + 0.000f, 0.000f, 0.000f, 1.000f, + 0.000f, 0.000f, 0.383f, 0.924f, + 0.000f, 0.000f, 0.707f, 0.707f, + 0.000f, 0.000f, 0.707f, 0.707f, + 0.000f, 0.000f, 0.383f, 0.924f, + 0.000f, 0.000f, 0.000f, 1.000f, + 0.000f, 0.000f, 0.000f, 1.000f, + 0.000f, 0.000f, -0.383f, 0.924f, + 0.000f, 0.000f, -0.707f, 0.707f, + 0.000f, 0.000f, -0.707f, 0.707f, + 0.000f, 0.000f, -0.383f, 0.924f, + 0.000f, 0.000f, 0.000f, 1.000f}; + // clang-format on + ASSERT_EQ(node_animation_data, expected_node_animation_data); + } + + // Check skin. + const Skin *const skin = scene->GetSkin(SkinIndex(0)); + ASSERT_NE(skin, nullptr); + ASSERT_EQ(skin->NumJoints(), 2); + ASSERT_EQ(skin->GetJointRoot(), kInvalidSceneNodeIndex); + ASSERT_EQ(skin->GetJoint(0), SceneNodeIndex(1)); + ASSERT_EQ(skin->GetJoint(1), SceneNodeIndex(2)); + + // Check inverse bind matrices. + const NodeAnimationData &bind_matrices = skin->GetInverseBindMatrices(); + ASSERT_EQ(bind_matrices.type(), NodeAnimationData::Type::MAT4); + ASSERT_EQ(bind_matrices.count(), 2); + ASSERT_EQ(bind_matrices.normalized(), false); + ASSERT_NE(bind_matrices.GetData(), nullptr); + const std::vector &bind_matrices_data = *bind_matrices.GetData(); + // clang-format off + const std::vector expected_bind_matrices_data{ + // First matrix. + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + -0.5f, -1.0f, 0.0f, 1.0f, + // Second matrix. + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + -0.5f, -1.0f, 0.0f, 1.0f}; + // clang-format on + ASSERT_EQ(bind_matrices_data, expected_bind_matrices_data); + + // Check mesh size. + const Mesh &mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_EQ(mesh.num_faces(), 8); + ASSERT_EQ(mesh.num_points(), 10); + ASSERT_EQ(mesh.num_attributes(), 3); + + // Check vertex joint indices. + const PointAttribute *const joints_att = + mesh.GetNamedAttribute(GeometryAttribute::JOINTS); + ASSERT_NE(joints_att, nullptr); + ASSERT_EQ(joints_att->data_type(), DT_UINT16); + ASSERT_EQ(joints_att->num_components(), 4); + ASSERT_EQ(joints_att->size(), 1); + // clang-format off + const std::array expected_joints = { + // Each vertex is associated with four joints. + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0, + 0, 1, 0, 0 }; + // clang-format on + std::array joints; + for (draco::PointIndex pi(0); pi < mesh.num_points(); ++pi) { + joints_att->GetMappedValue(pi, &joints[4 * pi.value()]); + } + ASSERT_EQ(joints, expected_joints); + + // Check vertex joint weights. + const PointAttribute *const weights_att = + mesh.GetNamedAttribute(GeometryAttribute::WEIGHTS); + ASSERT_NE(weights_att, nullptr); + ASSERT_EQ(weights_att->data_type(), DT_FLOAT32); + ASSERT_EQ(weights_att->num_components(), 4); + ASSERT_EQ(weights_att->size(), 5); + // clang-format off + const std::array expected_weights = { + // Each vertex has four joint weights. + 1.00f, 0.00f, 0.00f, 0.00f, + 1.00f, 0.00f, 0.00f, 0.00f, + 0.75f, 0.25f, 0.00f, 0.00f, + 0.75f, 0.25f, 0.00f, 0.00f, + 0.50f, 0.50f, 0.00f, 0.00f, + 0.50f, 0.50f, 0.00f, 0.00f, + 0.25f, 0.75f, 0.00f, 0.00f, + 0.25f, 0.75f, 0.00f, 0.00f, + 0.00f, 1.00f, 0.00f, 0.00f, + 0.00f, 1.00f, 0.00f, 0.00f }; + // clang-format on + std::array weights; + for (draco::PointIndex pi(0); pi < mesh.num_points(); ++pi) { + weights_att->GetMappedValue(pi, &weights[4 * pi.value()]); + } + ASSERT_EQ(weights, expected_weights); +} + +TEST(GltfDecoderTest, DecodeMeshWithImplicitPrimitiveIndices) { + // Check that glTF primitives with implicit indices can be loaded as a mesh. + const std::string file_name = "Fox/glTF/Fox.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_faces(), 576); +} + +TEST(GltfDecoderTest, DecodeSceneWithImplicitPrimitiveIndices) { + // Check that glTF primitives with implicit indices can be loaded as a scene. + const std::string file_name = "Fox/glTF/Fox.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->GetMesh(MeshIndex(0)).num_faces(), 576); +} + +TEST(GltfDecoderTest, DecodeFromBufferToMesh) { + // Checks that a mesh can be decoded from buffer in GLB format. + // Read GLB file contents into a buffer. + const std::string file_name = "KhronosSampleModels/Duck/glTF_Binary/Duck.glb"; + const std::string file_path = GetTestFileFullPath(file_name); + std::vector file_data; + ASSERT_TRUE(ReadFileToBuffer(file_path, &file_data)); + DecoderBuffer buffer; + buffer.Init(file_data.data(), file_data.size()); + + // Decode mesh from buffer. + GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromBuffer(&buffer)); + ASSERT_NE(mesh, nullptr); + + // Decode mesh from GLB file. + const std::unique_ptr expected_mesh(DecodeGltfFile(file_name)); + ASSERT_NE(expected_mesh, nullptr); + + // Check that meshes decoded from the buffer and from GLB file are equivalent. + MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, *expected_mesh)); +} + +TEST(GltfDecoderTest, DecodeGraph) { + // Checks that we can decode a scene with a general graph structure where a + // node has multiple parents. + // The input model has one root node, 4 children nodes that all point to a + // single node that contains the cube mesh. + const std::string file_name = "CubeScaledInstances/glTF/cube_att.gltf"; + const std::string file_path = GetTestFileFullPath(file_name); + + // First decode the scene into a tree-graph. + draco::GltfDecoder dec_tree; + DRACO_ASSIGN_OR_ASSERT(auto scene_tree, + dec_tree.DecodeFromFileToScene(file_path)); + // We expect to have 9 nodes with 4 mesh instances. The leaf node with the + // cube is duplicated 4 times, once for each instance. + ASSERT_EQ(scene_tree->NumNodes(), 9); + auto instances_tree = draco::SceneUtils::ComputeAllInstances(*scene_tree); + ASSERT_EQ(instances_tree.size(), 4); + + // Decode the scene into a scene-graph. + draco::GltfDecoder dec_graph; + dec_graph.SetSceneGraphMode(draco::GltfDecoder::GltfSceneGraphMode::DAG); + DRACO_ASSIGN_OR_ASSERT(auto scene_graph, + dec_graph.DecodeFromFileToScene(file_path)); + + // We expect to have 6 nodes with 4 mesh instances. The leaf node is shared + // for all mesh instances. + ASSERT_EQ(scene_graph->NumNodes(), 6); + auto instances_graph = draco::SceneUtils::ComputeAllInstances(*scene_graph); + ASSERT_EQ(instances_graph.size(), 4); + + // Check that all instances share the same scene node. + for (draco::MeshInstanceIndex mii(1); mii < 4; ++mii) { + ASSERT_EQ(instances_graph[mii - 1].scene_node_index, + instances_graph[mii].scene_node_index); + } +} + +TEST(GltfDecoderTest, CorrectVolumeThicknessFactor) { + // Checks that when a model is decoded as draco::Mesh the PBR material volume + // thickness factor is corrected according to geometry transformation scale in + // the scene graph. + constexpr float kDragonScale = 0.25f; + constexpr float kDragonVolumeThickness = 2.27f; + + // Read model as draco::Scene and check dragon mesh transformation scale and + // its PBR material volume thickness factor. + const std::unique_ptr scene = draco::ReadSceneFromTestFile( + "KhronosSampleModels/DragonAttenuation/glTF/DragonAttenuation.gltf"); + ASSERT_NE(scene, nullptr); + auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 2); + ASSERT_EQ(instances[MeshInstanceIndex(0)].transform.col(0).norm(), + kDragonScale); + ASSERT_EQ(scene->GetMaterialLibrary().GetMaterial(1)->GetThicknessFactor(), + kDragonVolumeThickness); + + // Read model as draco::Mesh and check corrected volume thickness factor. + const std::unique_ptr mesh = draco::ReadMeshFromTestFile( + "KhronosSampleModels/DragonAttenuation/glTF/DragonAttenuation.gltf"); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(1)->GetThicknessFactor(), + kDragonScale * kDragonVolumeThickness); +} + +TEST(GltfDecoderTest, DecodeLightsIntoMesh) { + // Checks that a model with lights can be decoded into draco::Mesh with the + // lights discarded. + const std::string file_name = "sphere_lights.gltf"; + const std::unique_ptr mesh(DecodeGltfFile(file_name)); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_faces(), 224); +} + +TEST(GltfDecoderTest, DecodeLightsIntoScene) { + // Checks that a model with lights can be decoded into draco::Scene. + const std::string file_name = "sphere_lights.gltf"; + const std::unique_ptr scene(DecodeGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumLights(), 4); + + // Check spot light with all properties specified. + Light &light = *scene->GetLight(LightIndex(0)); + ASSERT_EQ(light.GetName(), "Blue Lightsaber"); + ASSERT_EQ(light.GetColor(), draco::Vector3f(0.72f, 0.71f, 1.00f)); + ASSERT_EQ(light.GetIntensity(), 3.0); + ASSERT_EQ(light.GetType(), draco::Light::SPOT); + ASSERT_EQ(light.GetRange(), 100); + ASSERT_EQ(light.GetInnerConeAngle(), 0.2); + ASSERT_EQ(light.GetOuterConeAngle(), 0.8); + + // Check point light with all properties specified. + light = *scene->GetLight(LightIndex(1)); + ASSERT_EQ(light.GetName(), "The Star of Earendil"); + ASSERT_EQ(light.GetColor(), draco::Vector3f(0.90f, 0.97f, 1.0f)); + ASSERT_EQ(light.GetIntensity(), 5.0); + ASSERT_EQ(light.GetType(), draco::Light::POINT); + ASSERT_EQ(light.GetRange(), 1000); + ASSERT_EQ(light.GetInnerConeAngle(), 0.0); + ASSERT_NEAR(light.GetOuterConeAngle(), DRACO_PI / 4.0f, 1e-8); + + // Check directional light with some properties specified. + light = *scene->GetLight(LightIndex(2)); + ASSERT_EQ(light.GetName(), "Arc Reactor"); + ASSERT_EQ(light.GetColor(), draco::Vector3f(0.9f, 0.9, 0.9f)); + ASSERT_EQ(light.GetIntensity(), 1.0); + ASSERT_EQ(light.GetType(), draco::Light::DIRECTIONAL); + ASSERT_EQ(light.GetRange(), 200.0); + + // Check spot light with no properties specified. + light = *scene->GetLight(LightIndex(3)); + ASSERT_EQ(light.GetName(), ""); + ASSERT_EQ(light.GetColor(), draco::Vector3f(1.0f, 1.0f, 1.0f)); + ASSERT_EQ(light.GetIntensity(), 1.0); + ASSERT_EQ(light.GetType(), draco::Light::SPOT); + ASSERT_EQ(light.GetRange(), std::numeric_limits::max()); + ASSERT_EQ(light.GetInnerConeAngle(), 0.0); + ASSERT_NEAR(light.GetOuterConeAngle(), DRACO_PI / 4.0f, 1e-8); + + // Check that lights are referenced by the scene nodes. + ASSERT_EQ(scene->GetNode(SceneNodeIndex(0))->GetLightIndex(), + kInvalidLightIndex); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(1))->GetLightIndex(), LightIndex(0)); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(2))->GetLightIndex(), LightIndex(2)); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(3))->GetLightIndex(), LightIndex(3)); + ASSERT_EQ(scene->GetNode(SceneNodeIndex(4))->GetLightIndex(), LightIndex(1)); +} + +TEST(GltfDecoderTest, MaterialsVariants) { + // Checks that a model with KHR_materials_variants extension can be decoded. + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, + decoder.DecodeFromFileToScene(GetTestFileFullPath( + "KhronosSampleModels/DragonAttenuation/glTF/" + "DragonAttenuation.gltf"))); + ASSERT_NE(scene, nullptr); + const draco::MaterialLibrary &library = scene->GetMaterialLibrary(); + ASSERT_EQ(library.NumMaterialsVariants(), 2); + ASSERT_EQ(library.GetMaterialsVariantName(0), "Attenuation"); + ASSERT_EQ(library.GetMaterialsVariantName(1), "Surface Color"); + + // Check that the cloth mesh has no material variants. + const draco::MeshGroup &cloth_group = + *scene->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(cloth_group.GetName(), "Cloth Backdrop"); + ASSERT_EQ(cloth_group.NumMeshInstances(), 1); + const auto &cloth_mappings = + cloth_group.GetMeshInstance(0).materials_variants_mappings; + ASSERT_EQ(cloth_mappings.size(), 0); + + // Check that the dragon has correct materials variants. + const draco::MeshGroup &dragon_group = + *scene->GetMeshGroup(draco::MeshGroupIndex(1)); + ASSERT_EQ(dragon_group.GetName(), "Dragon"); + ASSERT_EQ(dragon_group.NumMeshInstances(), 1); + const auto &dragon_mappings = + dragon_group.GetMeshInstance(0).materials_variants_mappings; + ASSERT_EQ(dragon_mappings.size(), 2); + ASSERT_EQ(dragon_mappings[0].material, 1); + ASSERT_EQ(dragon_mappings[1].material, 2); + ASSERT_EQ(dragon_mappings[0].variants.size(), 1); + ASSERT_EQ(dragon_mappings[1].variants.size(), 1); + ASSERT_EQ(dragon_mappings[0].variants[0], 0); + ASSERT_EQ(dragon_mappings[1].variants[0], 1); +} + +TEST(GltfDecoderTest, DecodeMeshWithMeshFeaturesWithStructuralMetadata) { + // Checks decoding of a simple glTF with mesh features and structural metadata + // property table as draco::Mesh. + constexpr bool kDracoCompressionEnabled = false; + const auto path = GetTestFileFullPath("BoxMeta/glTF/BoxMeta.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path)); + ASSERT_NE(mesh, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh, kDracoCompressionEnabled); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*mesh); +} + +TEST(GltfDecoderTest, DecodeMeshWithMeshFeaturesWithDracoCompression) { + // Checks decoding of a simple glTF with mesh features compressed with Draco + // as draco::Mesh. + constexpr bool kDracoCompressionEnabled = true; + const auto path = GetTestFileFullPath("BoxMetaDraco/glTF/BoxMetaDraco.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path)); + ASSERT_NE(mesh, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh, kDracoCompressionEnabled); +} + +TEST(GltfDecoderTest, DecodeSceneWithMeshFeaturesWithStructuralMetadata) { + // Checks decoding of a simple glTF with mesh features and structural metadata + // property table as draco::Scene. + constexpr bool kHasDracoCompression = false; + const auto path = GetTestFileFullPath("BoxMeta/glTF/BoxMeta.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene, kHasDracoCompression); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*scene); +} + +TEST(GltfDecoderTest, DecodeSceneWithMeshFeaturesWithDracoCompression) { + // Checks decoding of a simple glTF with mesh features compressed with Draco + // as draco::Scene. + constexpr bool kHasDracoCompression = true; + const auto path = GetTestFileFullPath("BoxMetaDraco/glTF/BoxMetaDraco.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene, kHasDracoCompression); +} + +TEST(GltfDecoderTest, DecodePointCloudToMesh) { + // Checks decoding of a simple glTF with point primitives (no meshes). + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_point_cloud.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto mesh, decoder.DecodeFromFile(path)); + ASSERT_NE(mesh, nullptr); + + // Check the point cloud has expected number of points and attributes. + ASSERT_EQ(mesh->num_faces(), 0); + ASSERT_EQ(mesh->num_points(), 462); + + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), 1); + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::TANGENT), 1); + ASSERT_EQ(mesh->NumNamedAttributes(draco::GeometryAttribute::MATERIAL), 1); + + // Check the point cloud has two materials. + ASSERT_EQ(mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL)->size(), + 2); +} + +TEST(GltfDecoderTest, DecodeMeshAndPointCloudToMesh) { + // Checks decoding of a simple glTF with a mesh and point primitives into + // draco::Mesh. This should fail (draco::Mesh can't support mixed primitives). + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_mesh_and_point_cloud.gltf"); + draco::GltfDecoder decoder; + ASSERT_FALSE(decoder.DecodeFromFile(path).ok()); +} + +TEST(GltfDecoderTest, DecodePointCloudToScene) { + // Checks decoding of a simple glTF with point primitives (no meshes) into + // draco::Scene. + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_point_cloud.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + + ASSERT_EQ(scene->NumMeshes(), 2); + + // Check that each point cloud has expected number of points and attributes. + for (draco::MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + const auto &mesh = scene->GetMesh(mi); + ASSERT_EQ(mesh.num_faces(), 0); + ASSERT_EQ(mesh.num_points(), 231); + + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TANGENT), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::MATERIAL), 0); + } + + // Check the materials are properly assigned to each point cloud. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 2); + ASSERT_EQ(draco::SceneUtils::GetMeshInstanceMaterialIndex( + *scene, instances[draco::MeshInstanceIndex(0)]), + 0); + ASSERT_EQ(draco::SceneUtils::GetMeshInstanceMaterialIndex( + *scene, instances[draco::MeshInstanceIndex(1)]), + 1); +} + +TEST(GltfDecoderTest, DecodeMeshAndPointCloudToScene) { + // Checks decoding of a simple glTF with a mesh and point primitives into + // draco::Scene. + const auto path = GetTestFileFullPath( + "SphereTwoMaterials/sphere_two_materials_mesh_and_point_cloud.gltf"); + draco::GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto scene, decoder.DecodeFromFileToScene(path)); + ASSERT_NE(scene, nullptr); + + ASSERT_EQ(scene->NumMeshes(), 2); + + // First mesh should be a real mesh while the other one should be a point + // cloud (no faces). Otherwise, they should have the same properties. + for (draco::MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + const auto &mesh = scene->GetMesh(mi); + ASSERT_EQ(mesh.num_faces(), mi.value() == 0 ? 224 : 0); + ASSERT_EQ(mesh.num_points(), 231); + + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), 1); + ASSERT_EQ(mesh.NumNamedAttributes(draco::GeometryAttribute::TANGENT), 1); + } +} + +TEST(GltfDecoderTest, TestLoadUnsupportedTexCoordAttributes) { + // Checks that unsupported attributes (TEXCOORD_2 ... TEXCOORD_7) are ignored + // without causing the decoder to fail. + auto scene = draco::ReadSceneFromTestFile("UnusedTexCoords/TexCoord2.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 2); +} + +} // namespace draco +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_encoder.cc b/contrib/draco/src/draco/io/gltf_encoder.cc new file mode 100644 index 000000000..0509b588f --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_encoder.cc @@ -0,0 +1,3662 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_encoder.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "draco/attributes/geometry_attribute.h" +#include "draco/attributes/point_attribute.h" +#include "draco/compression/draco_compression_options.h" +#include "draco/compression/expert_encode.h" +#include "draco/core/draco_types.h" +#include "draco/core/vector_d.h" +#include "draco/io/file_utils.h" +#include "draco/io/file_writer_utils.h" +#include "draco/io/gltf_utils.h" +#include "draco/io/texture_io.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/mesh_splitter.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/instance_array.h" +#include "draco/scene/scene_indices.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +// Values are specfified from glTF 2.0 sampler spec. See here for more +// information: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#sampler +int TextureFilterTypeToGltfValue(TextureMap::FilterType filter_type) { + switch (filter_type) { + case TextureMap::NEAREST: + return 9728; + case TextureMap::LINEAR: + return 9729; + case TextureMap::NEAREST_MIPMAP_NEAREST: + return 9984; + case TextureMap::LINEAR_MIPMAP_NEAREST: + return 9985; + case TextureMap::NEAREST_MIPMAP_LINEAR: + return 9986; + case TextureMap::LINEAR_MIPMAP_LINEAR: + return 9987; + default: + return -1; + } +} + +// Values are specfified from glTF 2.0 sampler spec. See here for more +// information: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#sampler +int TextureAxisWrappingModeToGltfValue(TextureMap::AxisWrappingMode mode) { + switch (mode) { + case TextureMap::CLAMP_TO_EDGE: + return 33071; + case TextureMap::MIRRORED_REPEAT: + return 33648; + case TextureMap::REPEAT: + return 10497; + default: + return -1; + } +} + +// Checks |att| metadata entry in |mesh| with key "attribute_name" and returns +// entry value if it begins with "_FEATURE_ID_", or an empty string otherwise. +std::string GetFeatureIdAttributeName(const PointAttribute &att, + const Mesh &mesh) { + const auto *const metadata = + mesh.GetAttributeMetadataByAttributeId(att.unique_id()); + if (metadata) { + std::string attribute_name; + if (metadata->GetEntryString("attribute_name", &attribute_name)) { + constexpr char kPrefix[] = "_FEATURE_ID_"; + if (attribute_name.rfind(kPrefix) == 0) { + return attribute_name; + } + } + } + return std::string(); +} + +// Struct to hold glTF Scene data. +struct GltfScene { + std::vector node_indices; +}; + +// Struct to hold glTF Node data. +struct GltfNode { + GltfNode() + : mesh_index(-1), + skin_index(-1), + light_index(-1), + instance_array_index(-1), + root_node(false) {} + + std::string name; + std::vector childern_indices; + int mesh_index; + int skin_index; + int light_index; + int instance_array_index; + bool root_node; + TrsMatrix trs_matrix; +}; + +// Struct to hold image data. +struct GltfImage { + std::string image_name; + const Texture *texture; + std::unique_ptr owned_texture; + int num_components = 0; + int buffer_view = -1; + std::string mime_type; +}; + +// Struct to hold texture filtering options. The members are based on glTF 2.0 +// samplers. For more information see: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplers +struct TextureSampler { + TextureSampler(TextureMap::FilterType min, TextureMap::FilterType mag, + TextureMap::WrappingMode mode) + : min_filter(min), mag_filter(mag), wrapping_mode(mode) {} + + bool operator==(const TextureSampler &other) const { + if (min_filter != other.min_filter) { + return false; + } + if (mag_filter != other.mag_filter) { + return false; + } + return wrapping_mode.s == other.wrapping_mode.s && + wrapping_mode.t == other.wrapping_mode.t; + } + + TextureMap::FilterType min_filter = TextureMap::UNSPECIFIED; + TextureMap::FilterType mag_filter = TextureMap::UNSPECIFIED; + TextureMap::WrappingMode wrapping_mode = {TextureMap::CLAMP_TO_EDGE, + TextureMap::CLAMP_TO_EDGE}; +}; + +// Struct to hold texture data. Multiple textures can reference the same image. +struct GltfTexture { + GltfTexture(int image, int sampler) + : image_index(image), sampler_index(sampler) {} + bool operator==(const GltfTexture &other) const { + return image_index == other.image_index && + sampler_index == other.sampler_index; + } + int image_index; + int sampler_index; +}; + +// Struct to hold glTF Accessor data. +struct GltfAccessor { + GltfAccessor() + : buffer_view_index(-1), + byte_stride(0), + component_type(-1), + normalized(false) {} + + int buffer_view_index; + int byte_stride; + int component_type; + int64_t count; + std::vector max; + std::vector min; + std::string type; + bool normalized; +}; + +// Struct to hold glTF BufferView data. Currently there is only one Buffer, so +// there is no need to store a buffer index. +struct GltfBufferView { + int64_t buffer_byte_offset = -1; + int64_t byte_length = 0; + int target = 0; +}; + +// Struct to hold information about a Draco compressed mesh. +struct GltfDracoCompressedMesh { + int buffer_view_index = -1; + std::map attributes; +}; + +// Struct to hold glTF Primitive data. +struct GltfPrimitive { + GltfPrimitive() : indices(-1), mode(4), material(0) {} + + int indices; + int mode; + int material; + std::vector material_variants_mappings; + std::vector mesh_features; + std::map attributes; + GltfDracoCompressedMesh compressed_mesh_info; +}; + +struct GltfMesh { + std::string name; + std::vector primitives; +}; + +// Class to hold and output glTF data. +class GltfAsset { + public: + // glTF value types and values. + enum ComponentType { + BYTE = 5120, + UNSIGNED_BYTE = 5121, + SHORT = 5122, + UNSIGNED_SHORT = 5123, + UNSIGNED_INT = 5125, + FLOAT = 5126 + }; + // Return the size of the component based on |max_value|. + static int UnsignedIntComponentSize(unsigned int max_value); + + // Return component type based on |max_value|. + static ComponentType UnsignedIntComponentType(unsigned int max_value); + + GltfAsset(); + + std::string generator() const { return generator_; } + std::string version() const { return version_; } + std::string buffer_name() const { return buffer_name_; } + void buffer_name(const std::string &name) { buffer_name_ = name; } + const EncoderBuffer *Buffer() const { return &buffer_; } + + // Convert a Draco Mesh to glTF data. + bool AddDracoMesh(const Mesh &mesh); + + // Convert a Draco Scene to glTF data. + Status AddScene(const Scene &scene); + + // Copy the glTF data to |buf_out|. + Status Output(EncoderBuffer *buf_out); + + // Return the output image referenced by |index|. + const GltfImage *GetImage(int index) const; + + // Return the number of images added to the GltfAsset. + int NumImages() const { return images_.size(); } + + const std::string &image_name(int i) const { return images_[i].image_name; } + + void set_add_images_to_buffer(bool flag) { add_images_to_buffer_ = flag; } + bool add_images_to_buffer() const { return add_images_to_buffer_; } + void set_output_type(GltfEncoder::OutputType type) { output_type_ = type; } + GltfEncoder::OutputType output_type() const { return output_type_; } + void set_json_output_mode(JsonWriter::Mode mode) { gltf_json_.SetMode(mode); } + + private: + // Pad |buffer_| to 4 byte boundary. + bool PadBuffer(); + + // Returns the index of the scene that was added. -1 on error. + int AddScene(); + + // Add a glTF attribute index to |draco_extension|. + void AddAttributeToDracoExtension( + const Mesh &mesh, GeometryAttribute::Type type, int index, + const std::string &name, GltfDracoCompressedMesh *compressed_mesh_info); + + // Compresses |mesh| using Draco. On success returns the buffer_view in + // |primitive| and number of encoded points and faces. + Status CompressMeshWithDraco(const Mesh &mesh, + const Eigen::Matrix4d &transform, + GltfPrimitive *primitive, + int64_t *num_encoded_points, + int64_t *num_encoded_faces); + + // Adds a Draco mesh associated with a material id and material variants. + bool AddDracoMesh(const Mesh &mesh, int material_id, + const std::vector + &material_variants_mappings, + const Eigen::Matrix4d &transform); + + // Add the Draco mesh indices to the glTF data. |num_encoded_faces| is the + // number of faces encoded in |mesh|, which can be different than + // mesh.numfaces(). Returns the index of the accessor that was added. -1 on + // error. + int AddDracoIndices(const Mesh &mesh, int64_t num_encoded_faces); + + // Add the Draco mesh positions attribute to the glTF data. + // |num_encoded_points| is the number of points encoded in |mesh|, which can + // be different than mesh.num_points(). Returns the index of the accessor that + // was added. -1 on error. + int AddDracoPositions(const Mesh &mesh, int num_encoded_points); + + // Add the Draco mesh normals attribute to the glTF data. |num_encoded_points| + // is the number of points encoded in |mesh|, which can be different than + // mesh.num_points(). Returns the index of + // the accessor that was added. -1 on error. + int AddDracoNormals(const Mesh &mesh, int num_encoded_points); + + // Add the Draco mesh vertex color attribute to the glTF data. + // |num_encoded_points| is the number of points encoded in |mesh|, which can + // be different than mesh.num_points(). Returns the index of the accessor that + // was added. -1 on error. + int AddDracoColors(const Mesh &mesh, int num_encoded_points); + + // Add the Draco mesh texture attribute to the glTF data. |tex_coord_index| is + // the index into the texture coordinates added to |mesh|. + // |num_encoded_points| is the number of points encoded in |mesh|, which can + // be different than mesh.num_points(). Returns the index of the accessor that + // was added. -1 on error. + int AddDracoTexture(const Mesh &mesh, int tex_coord_index, + int num_encoded_points); + + // Add the Draco mesh tangent attribute to the glTF data. The Draco mesh + // tangents only contains the x, y, and z components and glTF needs the + // x, y, z, and w components for glTF mesh tangents. Note this is not true + // for tangents of glTF morph targets. This function will add the w component + // to the glTF tangents. |num_encoded_points| is the + // number of points encoded in |mesh|, which can be different than + // mesh.num_points(). Returns the index of the accessor that was added. + // -1 on error. + // Note: Tangents are not added if the attribute contains "auto_generated" + // metadata. See go/tangents_and_draco_simplifier for more details. + int AddDracoTangents(const Mesh &mesh, int num_encoded_points); + + int AddDracoJoints(const Mesh &mesh, int num_encoded_points); + int AddDracoWeights(const Mesh &mesh, int num_encoded_points); + std::vector> AddDracoGenerics( + const Mesh &mesh, int num_encoded_points); + + // Iterate through the materials that are associated with |mesh| and add them + // to the asset. Returns true if |mesh| does not contain any materials or all + // the materials are supported. Returns false if |mesh| contains materials + // that are not supported. + bool AddMaterials(const Mesh &mesh); + + // Checks whether a given Draco |attribute| has data of expected |data_type| + // and whether the data has one of expected |num_components|. Returns true + // when the |attribute| meets expectations, false otherwise. + static bool CheckDracoAttribute(const PointAttribute *attribute, + const std::set &data_types, + const std::set &num_components); + + // Returns the name of |texture|. If |texture|'s name is empty then it will + // generate a name using |texture_index| and |suffix|. If it cannot generate a + // name then it will return an empty string. + std::string GetTextureName(const Texture &texture, int texture_index, + const std::string &suffix) const; + + // Adds a new glTF image to the asset and returns its index. |owned_texture| + // is an optional argument that can be used when the added image is not + // contained in the encoded MaterialLibrary (e.g. for images that are locally + // modified before they are encoded to disk). The image file name is generated + // by combining |image_stem| and image mime type contained in the |texture|. + StatusOr AddImage(const std::string &image_stem, const Texture *texture, + int num_components); + StatusOr AddImage(const std::string &image_stem, const Texture *texture, + std::unique_ptr owned_texture, + int num_components); + + // Saves an image with a given |image_index| into a buffer. + Status SaveImageToBuffer(int image_index); + + // Adds |sampler| to vector of samplers and returns the index. If |sampler| is + // equal to default values then |sampler| is not added to the vector and + // returns -1. + StatusOr AddTextureSampler(const TextureSampler &sampler); + + // Adds a Draco SceneNode, referenced by |scene_node_index|, to the glTF data. + Status AddSceneNode(const Scene &scene, SceneNodeIndex scene_node_index); + + // Iterate through the materials that are associated with |scene| and add them + // to the asset. Returns true if |scene| does not contain any materials or all + // the materials are supported. Returns false if |scene| contains materials + // that are not supported. + bool AddMaterials(const Scene &scene); + + // Iterate through the animations that are associated with |scene| and add + // them to the asset. Returns OkStatus() if |scene| does not contain any + // animations. + Status AddAnimations(const Scene &scene); + + // Converts the data associated with |node_animation_data| and adds that to + // the encoder as an accessor. + StatusOr AddNodeAnimationData( + const NodeAnimationData &node_animation_data); + + // Iterate through the skins that are associated with |scene| and add + // them to the asset. Returns OkStatus() if |scene| does not contain any + // skins. + Status AddSkins(const Scene &scene); + + // Iterate through the lights that are associated with |scene| and add them to + // the asset. Returns OkStatus() if |scene| does not contain any lights. + Status AddLights(const Scene &scene); + + // Iterate through materials variants names that are associated with |scene| + // and add them to the asset. Returns OkStatus() if |scene| does not contain + // any materials variants. + Status AddMaterialsVariantsNames(const Scene &scene); + + // Iterate through the mesh group instance arrays that are associated with + // |scene| and add them to the asset. Returns OkStatus() if |scene| does not + // contain any mesh group instance arrays. + Status AddInstanceArrays(const Scene &scene); + + // Adds structural metadata from |geometry| to the asset, if any. + template + void AddStructuralMetadata(const GeometryT &geometry); + + // Adds float |data| representing |num_components|-length vectors to the + // encoder as accessor and return the new accessor index. + StatusOr AddData(const std::vector &data, int num_components); + + // Adds property table |data| as buffer view and returns buffer view index. + StatusOr AddBufferView(const PropertyTable::Property::Data &data); + + bool EncodeAssetProperty(EncoderBuffer *buf_out); + bool EncodeScenesProperty(EncoderBuffer *buf_out); + bool EncodeInitialSceneProperty(EncoderBuffer *buf_out); + bool EncodeNodesProperty(EncoderBuffer *buf_out); + Status EncodeMeshesProperty(EncoderBuffer *buf_out); + Status EncodePrimitiveExtensionsProperty(const GltfPrimitive &primitive, + EncoderBuffer *buf_out); + Status EncodeMaterials(EncoderBuffer *buf_out); + + // Encodes a color material. |red|, |green|, |blue|, |alpha|, and + // |metallic_factor| are values in the range of 0.0 - 1.0. + void EncodeColorMaterial(float red, float green, float blue, float alpha, + float metallic_factor); + Status EncodeDefaultMaterial(EncoderBuffer *buf_out); + + // Encodes a texture map. |object_name| is the name of the texture map. + // |image_index| is the index into the texture image array. |tex_coord_index| + // is the index into the texture coordinates. |texture_map| is a reference to + // the texture map that is going to be encoded. + Status EncodeTextureMap(const std::string &object_name, int image_index, + int tex_coord_index, const Material &material, + const TextureMap &texture_map); + + // Encodes a texture map similar to the method above. When the |object_name| + // is "texture" and |channels| is not empty, then the |channels| is encoded + // into the "channels" property as required by the "texture" object of the + // EXT_mesh_features extension. + Status EncodeTextureMap(const std::string &object_name, int image_index, + int tex_coord_index, const Material &material, + const TextureMap &texture_map, + const std::vector &channels); + Status EncodeMaterialsProperty(EncoderBuffer *buf_out); + + void EncodeMaterialUnlitExtension(const Material &material); + Status EncodeMaterialSheenExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialTransmissionExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialClearcoatExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialVolumeExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeMaterialIorExtension(const Material &material, + const Material &defaults); + Status EncodeMaterialSpecularExtension(const Material &material, + const Material &defaults, + int material_index); + Status EncodeTexture(const std::string &name, const std::string &stem_suffix, + TextureMap::Type type, int num_components, + const Material &material, int material_index); + Status EncodeAnimationsProperty(EncoderBuffer *buf_out); + Status EncodeSkinsProperty(EncoderBuffer *buf_out); + Status EncodeTopLevelExtensionsProperty(EncoderBuffer *buf_out); + Status EncodeLightsProperty(EncoderBuffer *buf_out); + Status EncodeMaterialsVariantsNamesProperty(EncoderBuffer *buf_out); + Status EncodeStructuralMetadataProperty(EncoderBuffer *buf_out); + bool EncodeAccessorsProperty(EncoderBuffer *buf_out); + bool EncodeBufferViewsProperty(EncoderBuffer *buf_out); + bool EncodeBuffersProperty(EncoderBuffer *buf_out); + Status EncodeExtensionsProperties(EncoderBuffer *buf_out); + + // Encodes a draco::VectorNX as a glTF array. + template + void EncodeVectorArray(const std::string &array_name, T vec) { + gltf_json_.BeginArray(array_name); + for (int i = 0; i < T::dimension; ++i) { + gltf_json_.OutputValue(vec[i]); + } + gltf_json_.EndArray(); + } + + // Add a mesh Draco attribute |att| that is comprised of floats to the glTF + // data. Returns the index accessor added to the glTF data. Returns -1 on + // error. + template + int AddAttribute(const PointAttribute &att, int num_points, + int num_encoded_points, bool compress) { + const int num_components = att.num_components(); + switch (num_components) { + case 1: + return AddAttribute<1, att_data_t>(att, num_points, num_encoded_points, + "SCALAR", compress); + break; + case 2: + return AddAttribute<2, att_data_t>(att, num_points, num_encoded_points, + "VEC2", compress); + break; + case 3: + return AddAttribute<3, att_data_t>(att, num_points, num_encoded_points, + "VEC3", compress); + break; + case 4: + return AddAttribute<4, att_data_t>(att, num_points, num_encoded_points, + "VEC4", compress); + break; + default: + break; + } + return -1; + } + + // Template method only has specialized implementations for known glTF types. + template + ComponentType GetComponentType() const = delete; + + template + int AddAttribute(const PointAttribute &att, int num_points, + int num_encoded_points, const std::string &type, + bool compress); + + std::string generator_; + std::string version_; + std::vector scenes_; + + // Initial scene to load. + int scene_index_; + + std::vector nodes_; + std::vector accessors_; + std::vector buffer_views_; + std::vector meshes_; + + // Data structure to copy the input meshes materials. + MaterialLibrary material_library_; + + std::vector images_; + std::vector textures_; + + std::unordered_map texture_to_image_index_map_; + + std::string buffer_name_; + EncoderBuffer buffer_; + JsonWriter gltf_json_; + + // Keeps track if the glTF mesh has been added. + std::map mesh_group_index_to_gltf_mesh_; + std::map> mesh_index_to_gltf_mesh_primitive_; + IndexTypeVector base_mesh_transforms_; + + struct EncoderAnimation { + std::string name; + std::vector> samplers; + std::vector> channels; + }; + std::vector> animations_; + + struct EncoderSkin { + EncoderSkin() : inverse_bind_matrices_index(-1), skeleton_index(-1) {} + int inverse_bind_matrices_index; + std::vector joints; + int skeleton_index; + }; + + // Instance array is represented by its attribute accessors. + struct EncoderInstanceArray { + EncoderInstanceArray() : translation(-1), rotation(-1), scale(-1) {} + int translation; + int rotation; + int scale; + }; + + std::vector> skins_; + std::vector> lights_; + std::vector materials_variants_names_; + std::vector instance_arrays_; + PropertyTable::Schema property_table_schema_; + std::vector property_tables_; + + // Indicates whether Draco compression is used for any of the asset meshes. + bool draco_compression_used_; + + // Indicates whether mesh features are used. + bool mesh_features_used_; + + // Counter for naming mesh feature textures. + int mesh_features_texture_index_; + + // If set GltfAsset will add the images to |buffer_| instead of writing the + // images to separate files. + bool add_images_to_buffer_; + + // Used to hold the extensions used and required by the glTF asset. + std::set extensions_used_; + std::set extensions_required_; + + std::vector texture_samplers_; + + GltfEncoder::OutputType output_type_; + + // Temporary storage for meshes created during the runtime of the GltfEncoder. + // We need to store them here to ensure their content doesn't get deleted + // before it is used by the encoder. + std::vector> local_meshes_; +}; + +int GltfAsset::UnsignedIntComponentSize(unsigned int max_value) { + // According to GLTF 2.0 spec, 0xff (and 0xffff respectively) are reserved for + // the primitive restart symbol. + if (max_value < 0xff) { + return 1; + } else if (max_value < 0xffff) { + return 2; + } + return 4; +} + +GltfAsset::ComponentType GltfAsset::UnsignedIntComponentType( + unsigned int max_value) { + // According to GLTF 2.0 spec, 0xff (and 0xffff respectively) are reserved for + // the primitive restart symbol. + if (max_value < 0xff) { + return UNSIGNED_BYTE; + } else if (max_value < 0xffff) { + return UNSIGNED_SHORT; + } + return UNSIGNED_INT; +} + +GltfAsset::GltfAsset() + : generator_("draco_decoder"), + version_("2.0"), + scene_index_(-1), + buffer_name_("buffer0.bin"), + draco_compression_used_(false), + mesh_features_used_(false), + mesh_features_texture_index_(0), + add_images_to_buffer_(false), + output_type_(GltfEncoder::COMPACT) {} + +bool GltfAsset::AddDracoMesh(const Mesh &mesh) { + const int scene_index = AddScene(); + if (scene_index < 0) { + return false; + } + if (!AddMaterials(mesh)) { + return false; + } + + GltfMesh gltf_mesh; + meshes_.push_back(gltf_mesh); + + AddStructuralMetadata(mesh); + + const int32_t material_att_id = + mesh.GetNamedAttributeId(GeometryAttribute::MATERIAL); + if (material_att_id == -1) { + if (!AddDracoMesh(mesh, 0, {}, Eigen::Matrix4d::Identity())) { + return false; + } + } else { + const auto mat_att = mesh.GetNamedAttribute(GeometryAttribute::MATERIAL); + + // Split mesh using the material attribute. + MeshSplitter splitter; + auto split_maybe = splitter.SplitMesh(mesh, material_att_id); + if (!split_maybe.ok()) { + return false; + } + auto split_meshes = std::move(split_maybe).value(); + for (int i = 0; i < split_meshes.size(); ++i) { + if (split_meshes[i] == nullptr) { + continue; // Empty mesh. Ignore. + } + uint32_t mat_index = 0; + mat_att->GetValue(AttributeValueIndex(i), &mat_index); + + // Copy over mesh features for a given material index. + Mesh::CopyMeshFeaturesForMaterial(mesh, split_meshes[i].get(), mat_index); + + // Move the split mesh to a temporary storage of the GltfAsset. This will + // ensure the mesh will stay alive as long the asset needs it. We have to + // do this because the split mesh may contain mesh features data that are + // used later in the encoding process. + local_meshes_.push_back(std::move(split_meshes[i])); + + // The material index in the glTF file corresponds to the index of the + // split mesh. + if (!AddDracoMesh(*(local_meshes_.back().get()), mat_index, {}, + Eigen::Matrix4d::Identity())) { + return false; + } + } + } + + // Currently output only one mesh. + GltfNode mesh_node; + mesh_node.mesh_index = 0; + nodes_.push_back(mesh_node); + nodes_.back().root_node = true; + return true; +} + +int GltfAsset::AddScene() { + GltfScene scene; + scenes_.push_back(scene); + const int scene_index = static_cast(scenes_.size()) - 1; + + if (scene_index_ == -1) { + scene_index_ = scene_index; + } + return scene_index; +} + +Status GltfAsset::Output(EncoderBuffer *buf_out) { + gltf_json_.BeginObject(); + if (!EncodeAssetProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding asset."); + } + if (!EncodeScenesProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding scenes."); + } + if (!EncodeInitialSceneProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding initial scene."); + } + if (!EncodeNodesProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding nodes."); + } + DRACO_RETURN_IF_ERROR(EncodeMeshesProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeMaterials(buf_out)); + if (!EncodeAccessorsProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding accessors."); + } + DRACO_RETURN_IF_ERROR(EncodeAnimationsProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeSkinsProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeTopLevelExtensionsProperty(buf_out)); + if (!EncodeBufferViewsProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding buffer views."); + } + if (!EncodeBuffersProperty(buf_out)) { + return Status(Status::DRACO_ERROR, "Failed encoding buffers."); + } + DRACO_RETURN_IF_ERROR(EncodeExtensionsProperties(buf_out)); + gltf_json_.EndObject(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Failed encoding json data."); + } + if (!buf_out->Encode("\n", 1)) { + return Status(Status::DRACO_ERROR, "Failed encoding json data."); + } + return OkStatus(); +} + +const GltfImage *GltfAsset::GetImage(int index) const { + if (index < 0 || index >= images_.size()) { + return nullptr; + } + return &images_[index]; +} + +bool GltfAsset::PadBuffer() { + if (buffer_.size() % 4 != 0) { + const int pad_bytes = 4 - buffer_.size() % 4; + const int pad_data = 0; + if (!buffer_.Encode(&pad_data, pad_bytes)) { + return false; + } + } + return true; +} + +void GltfAsset::AddAttributeToDracoExtension( + const Mesh &mesh, GeometryAttribute::Type type, int index, + const std::string &name, GltfDracoCompressedMesh *compressed_mesh_info) { + if (mesh.IsCompressionEnabled()) { + const PointAttribute *const att = mesh.GetNamedAttribute(type, index); + if (att) { + compressed_mesh_info->attributes.insert( + std::pair(name, att->unique_id())); + } + } +} + +Status GltfAsset::CompressMeshWithDraco(const Mesh &mesh, + const Eigen::Matrix4d &transform, + GltfPrimitive *primitive, + int64_t *num_encoded_points, + int64_t *num_encoded_faces) { + // Check that geometry comression options are valid. + DracoCompressionOptions compression_options = mesh.GetCompressionOptions(); + DRACO_RETURN_IF_ERROR(compression_options.Check()); + + // Make a copy of the mesh. It will be modified and compressed. + std::unique_ptr mesh_copy(new Mesh()); + mesh_copy->Copy(mesh); + + // Delete auto-generated tangents. + if (MeshUtils::HasAutoGeneratedTangents(*mesh_copy)) { + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::TANGENT) { + while (mesh_copy->GetNamedAttribute(GeometryAttribute::TANGENT)) { + mesh_copy->DeleteAttribute( + mesh_copy->GetNamedAttributeId(GeometryAttribute::TANGENT)); + } + break; + } + } + } + + // Create Draco encoder. + EncoderBuffer buffer; + ExpertEncoder encoder(*mesh_copy); + encoder.SetTrackEncodedProperties(true); + + // Convert compression level to speed (that 0 = slowest, 10 = fastest). + const int speed = 10 - compression_options.compression_level; + encoder.SetSpeedOptions(speed, speed); + + // Configure attribute quantization. + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + const PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::POSITION && + !compression_options.quantization_position + .AreQuantizationBitsDefined()) { + // Desired spacing in the "global" coordinate system. + const float global_spacing = + compression_options.quantization_position.spacing(); + + // Note: Ideally we would transform the whole mesh before encoding and + // apply the original global spacing on the transformed mesh. But neither + // KHR_draco_mesh_compression, nor Draco bitstream support post-decoding + // transformations so we have to modify the grid settings here. + + // Transform this spacing to the local coordinate system of the base mesh. + // We will get the largest scale factor from the transformation matrix and + // use it to adjust the grid spacing. + const Vector3f scale_vec(transform.col(0).norm(), transform.col(1).norm(), + transform.col(2).norm()); + + const float max_scale = scale_vec.MaxCoeff(); + + // Spacing is inverse to the scale. The larger the scale, the smaller the + // spacing must be. + const float local_spacing = global_spacing / max_scale; + + // Update the compression options of the processed mesh. + compression_options.quantization_position.SetGrid(local_spacing); + } else { + int num_quantization_bits = -1; + switch (att->attribute_type()) { + case GeometryAttribute::POSITION: + num_quantization_bits = + compression_options.quantization_position.quantization_bits(); + break; + case GeometryAttribute::NORMAL: + num_quantization_bits = compression_options.quantization_bits_normal; + break; + case GeometryAttribute::TEX_COORD: + num_quantization_bits = + compression_options.quantization_bits_tex_coord; + break; + case GeometryAttribute::TANGENT: + num_quantization_bits = compression_options.quantization_bits_tangent; + break; + case GeometryAttribute::WEIGHTS: + num_quantization_bits = compression_options.quantization_bits_weight; + break; + case GeometryAttribute::GENERIC: + if (GetFeatureIdAttributeName(*att, *mesh_copy).empty()) { + num_quantization_bits = + compression_options.quantization_bits_generic; + } else { + // Quantization is explicitly disabled for feature ID attributes. + encoder.SetAttributeQuantization(i, -1); + } + break; + default: + break; + } + if (num_quantization_bits > 0) { + encoder.SetAttributeQuantization(i, num_quantization_bits); + } + } + } + + // Flip UV values as required by glTF Draco and non-Draco files. + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::TEX_COORD) { + if (!MeshUtils::FlipTextureUvValues(false, true, att)) { + return Status(Status::DRACO_ERROR, "Could not flip texture UV values."); + } + } + } + + // Change tangents, joints, and weights attribute types to generic. The + // original mesh's attribute type is unchanged and the mapping of the glTF + // attribute type to Draco compressed attribute id is written to the output + // glTF file. + for (int i = 0; i < mesh_copy->num_attributes(); ++i) { + PointAttribute *const att = mesh_copy->attribute(i); + if (att->attribute_type() == GeometryAttribute::TANGENT || + att->attribute_type() == GeometryAttribute::JOINTS || + att->attribute_type() == GeometryAttribute::WEIGHTS) { + att->set_attribute_type(GeometryAttribute::GENERIC); + } + } + + // |compression_options| may have been modified and we need to update them + // before we start the encoding. + mesh_copy->SetCompressionOptions(compression_options); + DRACO_RETURN_IF_ERROR(encoder.EncodeToBuffer(&buffer)); + *num_encoded_points = encoder.num_encoded_points(); + *num_encoded_faces = encoder.num_encoded_faces(); + const size_t buffer_start_offset = buffer_.size(); + if (!buffer_.Encode(buffer.data(), buffer.size())) { + return Status(Status::DRACO_ERROR, "Could not copy Draco compressed data."); + } + if (!PadBuffer()) { + return Status(Status::DRACO_ERROR, "Could not pad glTF buffer."); + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + primitive->compressed_mesh_info.buffer_view_index = + static_cast(buffer_views_.size() - 1); + return OkStatus(); +} + +bool CheckAndGetTexCoordAttributeOrder(const Mesh &mesh, + std::vector *tex_coord_order) { + // We will only consider at most two texture coordinate attributes. + *tex_coord_order = {0, 1}; + const int num_attributes = + std::min(mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + + // Collect texture coordinate attribute names from metadata. + std::vector names(num_attributes, ""); + for (int i = 0; i < num_attributes; i++) { + const auto metadata = mesh.GetAttributeMetadataByAttributeId( + mesh.GetNamedAttributeId(GeometryAttribute::TEX_COORD, i)); + std::string attribute_name; + if (metadata != nullptr) { + metadata->GetEntryString("attribute_name", &attribute_name); + names[i] = attribute_name; + } + } + + // Attribute names may be absent. + if (num_attributes == 0 || + std::all_of(names.begin(), names.end(), + [](const std::string &name) { return name.empty(); })) { + return true; + } + + // Attribute names must be unique. + const std::unordered_set unique_names(names.begin(), + names.end()); + if (unique_names.size() != num_attributes) { + return false; + } + + // Attribute names must be valid. + if (std::any_of(names.begin(), names.end(), [](const std::string &name) { + return name != "TEXCOORD_0" && name != "TEXCOORD_1"; + })) { + return false; + } + + // Populate texture coordinate order index based on attribute names. + if (names[0] == "TEXCOORD_1") { + *tex_coord_order = {1, 0}; + } + return true; +} + +bool GltfAsset::AddDracoMesh( + const Mesh &mesh, int material_id, + const std::vector + &material_variants_mappings, + const Eigen::Matrix4d &transform) { + GltfPrimitive primitive; + int64_t num_encoded_points = mesh.num_points(); + int64_t num_encoded_faces = mesh.num_faces(); + if (num_encoded_faces > 0 && mesh.IsCompressionEnabled()) { + const Status status = CompressMeshWithDraco( + mesh, transform, &primitive, &num_encoded_points, &num_encoded_faces); + if (!status.ok()) { + return false; + } + draco_compression_used_ = true; + } + int indices_index = -1; + if (num_encoded_faces > 0) { + indices_index = AddDracoIndices(mesh, num_encoded_faces); + if (indices_index < 0) { + return false; + } + } + const int position_index = AddDracoPositions(mesh, num_encoded_points); + if (position_index < 0) { + return false; + } + // Check texture coordinate attributes and get the desired encoding order. + std::vector tex_coord_order; + if (!CheckAndGetTexCoordAttributeOrder(mesh, &tex_coord_order)) { + return false; + } + const int normals_accessor_index = AddDracoNormals(mesh, num_encoded_points); + const int colors_accessor_index = AddDracoColors(mesh, num_encoded_points); + const int texture0_accessor_index = + AddDracoTexture(mesh, tex_coord_order[0], num_encoded_points); + const int texture1_accessor_index = + AddDracoTexture(mesh, tex_coord_order[1], num_encoded_points); + const int tangent_accessor_index = AddDracoTangents(mesh, num_encoded_points); + const int joints_accessor_index = AddDracoJoints(mesh, num_encoded_points); + const int weights_accessor_index = AddDracoWeights(mesh, num_encoded_points); + const std::vector> generics_accessors = + AddDracoGenerics(mesh, num_encoded_points); + + if (num_encoded_faces == 0) { + primitive.mode = 0; // POINTS mode. + } + primitive.material = material_id; + primitive.material_variants_mappings = material_variants_mappings; + primitive.mesh_features.reserve(mesh.NumMeshFeatures()); + for (MeshFeaturesIndex i(0); i < mesh.NumMeshFeatures(); ++i) { + primitive.mesh_features.push_back(&mesh.GetMeshFeatures(i)); + } + primitive.indices = indices_index; + primitive.attributes.insert( + std::pair("POSITION", position_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::POSITION, 0, "POSITION", + &primitive.compressed_mesh_info); + if (normals_accessor_index > 0) { + primitive.attributes.insert( + std::pair("NORMAL", normals_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::NORMAL, 0, "NORMAL", + &primitive.compressed_mesh_info); + } + if (colors_accessor_index > 0) { + primitive.attributes.insert( + std::pair("COLOR_0", colors_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::COLOR, 0, "COLOR_0", + &primitive.compressed_mesh_info); + } + if (texture0_accessor_index > 0) { + primitive.attributes.insert( + std::pair("TEXCOORD_0", texture0_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::TEX_COORD, 0, + "TEXCOORD_0", &primitive.compressed_mesh_info); + } + if (texture1_accessor_index > 0) { + primitive.attributes.insert( + std::pair("TEXCOORD_1", texture1_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::TEX_COORD, 1, + "TEXCOORD_1", &primitive.compressed_mesh_info); + } + if (tangent_accessor_index > 0) { + primitive.attributes.insert( + std::pair("TANGENT", tangent_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::TANGENT, 0, "TANGENT", + &primitive.compressed_mesh_info); + } + if (joints_accessor_index > 0) { + primitive.attributes.insert( + std::pair("JOINTS_0", joints_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::JOINTS, 0, "JOINTS_0", + &primitive.compressed_mesh_info); + } + if (weights_accessor_index > 0) { + primitive.attributes.insert( + std::pair("WEIGHTS_0", weights_accessor_index)); + AddAttributeToDracoExtension(mesh, GeometryAttribute::WEIGHTS, 0, + "WEIGHTS_0", &primitive.compressed_mesh_info); + } + for (int att_index = 0; att_index < generics_accessors.size(); ++att_index) { + const std::string &attribute_name = generics_accessors[att_index].first; + if (!attribute_name.empty()) { + primitive.attributes.insert(generics_accessors[att_index]); + AddAttributeToDracoExtension(mesh, GeometryAttribute::GENERIC, att_index, + attribute_name, + &primitive.compressed_mesh_info); + } + } + + meshes_.back().primitives.push_back(primitive); + return true; +} + +int GltfAsset::AddDracoIndices(const Mesh &mesh, int64_t num_encoded_faces) { + // Get the min and max value for the indices. + uint32_t min_index = 0xffffffff; + uint32_t max_index = 0; + for (FaceIndex i(0); i < mesh.num_faces(); ++i) { + const auto &f = mesh.face(i); + + for (int j = 0; j < 3; ++j) { + if (f[j] < min_index) { + min_index = f[j].value(); + } + if (f[j] > max_index) { + max_index = f[j].value(); + } + } + } + + const int component_size = GltfAsset::UnsignedIntComponentSize(max_index); + + GltfAccessor accessor; + if (!mesh.IsCompressionEnabled()) { + const size_t buffer_start_offset = buffer_.size(); + for (FaceIndex i(0); i < mesh.num_faces(); ++i) { + const auto &f = mesh.face(i); + for (int j = 0; j < 3; ++j) { + int index = f[j].value(); + if (!buffer_.Encode(&index, component_size)) { + return -1; + } + } + } + + if (!PadBuffer()) { + return -1; + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + } + + accessor.component_type = UnsignedIntComponentType(max_index); + accessor.count = num_encoded_faces * 3; + if (output_type_ == GltfEncoder::VERBOSE) { + accessor.max.push_back(GltfValue(max_index)); + accessor.min.push_back(GltfValue(min_index)); + } + accessor.type = "SCALAR"; + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +int GltfAsset::AddDracoPositions(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::POSITION); + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {3})) { + return -1; + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoNormals(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::NORMAL); + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {3})) { + return -1; + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoColors(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::COLOR); + // TODO(b/200302561): Add support for DT_UINT16 with COLOR. + if (!CheckDracoAttribute(att, {DT_UINT8, DT_FLOAT32}, {3, 4})) { + return -1; + } + if (att->data_type() == DT_FLOAT32) { + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoTexture(const Mesh &mesh, int tex_coord_index, + int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::TEX_COORD, tex_coord_index); + // TODO(b/200303080): Add support for DT_UINT8 and DT_UINT16 with TEX_COORD. + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {2})) { + return -1; + } + + // glTF stores texture coordinates flipped on the horizontal axis compared to + // how Draco stores texture coordinates. + GeometryAttribute ga; + ga.Init(GeometryAttribute::TEX_COORD, nullptr, 2, att->data_type(), false, + DataTypeLength(att->data_type()) * 2, 0); + PointAttribute ta(ga); + ta.SetIdentityMapping(); + ta.Reset(mesh.num_points()); + + std::array value; + for (PointIndex v(0); v < mesh.num_points(); ++v) { + if (!att->GetValue(att->mapped_index(v), &value)) { + return -1; + } + + // Draco texture v component needs to be flipped. + Vector2f texture_coord(value[0], 1.0 - value[1]); + ta.SetAttributeValue(AttributeValueIndex(v.value()), texture_coord.data()); + } + return AddAttribute(ta, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoTangents(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::TANGENT); + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {3, 4})) { + return -1; + } + if (MeshUtils::HasAutoGeneratedTangents(mesh)) { + // Ignore auto-generated tangents. See go/tangents_and_draco_simplifier. + return -1; + } + + if (att->num_components() == 4) { + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + } + + // glTF mesh needs the w component. + GeometryAttribute ga; + ga.Init(GeometryAttribute::TANGENT, nullptr, 4, DT_FLOAT32, false, + DataTypeLength(DT_FLOAT32) * 4, 0); + PointAttribute ta(ga); + ta.SetIdentityMapping(); + ta.Reset(mesh.num_points()); + + std::array value; + for (PointIndex v(0); v < mesh.num_points(); ++v) { + if (!att->GetValue(att->mapped_index(v), &value)) { + return -1; + } + + // Draco tangent w component is always 1.0. + Vector4f tangent(value[0], value[1], value[2], 1.0); + ta.SetAttributeValue(AttributeValueIndex(v.value()), tangent.data()); + } + return AddAttribute(ta, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoJoints(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::JOINTS); + if (!CheckDracoAttribute(att, {DT_UINT8, DT_UINT16}, {4})) { + return -1; + } + if (att->data_type() == DT_UINT16) { + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +int GltfAsset::AddDracoWeights(const Mesh &mesh, int num_encoded_points) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::WEIGHTS); + // TODO(b/200303026): Add support for DT_UINT8 and DT_UINT16 with WEIGHTS. + if (!CheckDracoAttribute(att, {DT_FLOAT32}, {4})) { + return -1; + } + return AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); +} + +// Adds generic attributes that have metadata describing the attribute name. +// This allows for export of application-specific attributes and feature ID +// attributes defined in glTF extension EXT_mesh_features. Returns a vector of +// attribute-name, accessor pairs for each valid attribute. The length of the +// vector is equal to the number of generic attributes. Vector entries +// corresponding to unsupported attributes (e.g., with no metadata) contain +// empty attribute names. +std::vector> GltfAsset::AddDracoGenerics( + const Mesh &mesh, int num_encoded_points) { + const int num_attributes = + mesh.NumNamedAttributes(GeometryAttribute::GENERIC); + std::vector> attrs(num_attributes); + for (int i = 0; i < num_attributes; ++i) { + const PointAttribute *const att = + mesh.GetNamedAttribute(GeometryAttribute::GENERIC, i); + auto const *metadata = + mesh.GetAttributeMetadataByAttributeId(att->unique_id()); + if (metadata) { + std::string attr_name; + if (metadata->GetEntryString(GltfEncoder::kDracoMetadataGltfAttributeName, + &attr_name)) { + if (att->data_type() == DT_FLOAT32) { + int accessor = + AddAttribute(*att, mesh.num_points(), num_encoded_points, + mesh.IsCompressionEnabled()); + attrs[i] = {attr_name, accessor}; + } + } else { + // Try to find feature ID attribute name like "_FEATURE_ID_5" then check + // that the attribute stores scalar values of complient data types as + // defined by the EXT_mesh_features glTF extension. + attr_name = GetFeatureIdAttributeName(*att, mesh); + if (!attr_name.empty() && att->num_components() == 1) { + int accessor = -1; + switch (att->data_type()) { + case DT_UINT8: + accessor = AddAttribute(*att, mesh.num_points(), + num_encoded_points, + mesh.IsCompressionEnabled()); + break; + case DT_UINT16: + accessor = AddAttribute(*att, mesh.num_points(), + num_encoded_points, + mesh.IsCompressionEnabled()); + break; + case DT_FLOAT32: + accessor = AddAttribute(*att, mesh.num_points(), + num_encoded_points, + mesh.IsCompressionEnabled()); + break; + default: + continue; + } + attrs[i] = {attr_name, accessor}; + } + } + } + } + return attrs; +} + +bool GltfAsset::AddMaterials(const Mesh &mesh) { + if (mesh.GetMaterialLibrary().NumMaterials() == 0) { + return true; + } + material_library_.Copy(mesh.GetMaterialLibrary()); + return true; +} + +bool GltfAsset::CheckDracoAttribute(const PointAttribute *attribute, + const std::set &data_types, + const std::set &num_components) { + // Attribute must be valid. + if (attribute == nullptr || attribute->size() == 0) { + return false; + } + + // Attribute must have an expected data type. + if (data_types.find(attribute->data_type()) == data_types.end()) { + return false; + } + + // Attribute must have an expected number of components. + if (num_components.find(attribute->num_components()) == + num_components.end()) { + return false; + } + + return true; +} + +StatusOr GltfAsset::AddImage(const std::string &image_stem, + const Texture *texture, int num_components) { + return AddImage(image_stem, texture, nullptr, num_components); +} + +StatusOr GltfAsset::AddImage(const std::string &image_stem, + const Texture *texture, + std::unique_ptr owned_texture, + int num_components) { + const auto it = texture_to_image_index_map_.find(texture); + if (it != texture_to_image_index_map_.end()) { + // We already have an image for the given |texture|. Update its number of + // components if needed. + GltfImage &image = images_[it->second]; + if (image.num_components < num_components) { + image.num_components = num_components; + } + return it->second; + } + std::string extension = TextureUtils::GetTargetExtension(*texture); + if (extension.empty()) { + // Try to get extension from the source file name. + extension = LowercaseFileExtension(texture->source_image().filename()); + } + GltfImage image; + image.image_name = image_stem + "." + extension; + image.texture = texture; + image.owned_texture = std::move(owned_texture); + image.num_components = num_components; + + // Always maintain the mime_type. Used elsewhere to determine image type. + if (extension == "jpg") { + image.mime_type = "image/jpeg"; + } else { + image.mime_type = "image/" + extension; + } + + // For KTX2 with Basis compression, state that its extension is required. + if (extension == "ktx2") { + extensions_used_.insert("KHR_texture_basisu"); + extensions_required_.insert("KHR_texture_basisu"); + } + + // If this is webp, state that its extension is required. + if (extension == "webp") { + extensions_used_.insert("EXT_texture_webp"); + extensions_required_.insert("EXT_texture_webp"); + } + + images_.push_back(std::move(image)); + texture_to_image_index_map_[texture] = images_.size() - 1; + return images_.size() - 1; +} + +Status GltfAsset::SaveImageToBuffer(int image_index) { + GltfImage &image = images_[image_index]; + const Texture *const texture = image.texture; + const int num_components = image.num_components; + std::vector buffer; + DRACO_RETURN_IF_ERROR(WriteTextureToBuffer(*texture, &buffer)); + + // Add the image data to the buffer. + const size_t buffer_start_offset = buffer_.size(); + buffer_.Encode(buffer.data(), buffer.size()); + if (!PadBuffer()) { + return Status(Status::DRACO_ERROR, + "Could not pad buffer in SaveImageToBuffer."); + } + + // Add a buffer view pointing to the image data in the buffer. + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + + image.buffer_view = buffer_views_.size() - 1; + return OkStatus(); +} + +// TODO(vytyaz): The return type could be int. +StatusOr GltfAsset::AddTextureSampler(const TextureSampler &sampler) { + // If sampler is equal to defaults do not add to vector and return -1. + if (sampler.min_filter == TextureMap::UNSPECIFIED && + sampler.mag_filter == TextureMap::UNSPECIFIED && + sampler.wrapping_mode.s == TextureMap::REPEAT && + sampler.wrapping_mode.t == TextureMap::REPEAT) { + return -1; + } + + const auto &it = + std::find(texture_samplers_.begin(), texture_samplers_.end(), sampler); + if (it != texture_samplers_.end()) { + const int index = std::distance(texture_samplers_.begin(), it); + return index; + } + + texture_samplers_.push_back(sampler); + return texture_samplers_.size() - 1; +} + +Status GltfAsset::AddScene(const Scene &scene) { + const int scene_index = AddScene(); + if (scene_index < 0) { + return Status(Status::DRACO_ERROR, "Error creating a new scene."); + } + if (!AddMaterials(scene)) { + return Status(Status::DRACO_ERROR, "Error adding materials to the scene."); + } + // Initialize base mesh transforms that may be needed when the base meshes are + // compressed with Draco. + base_mesh_transforms_ = SceneUtils::FindLargestBaseMeshTransforms(scene); + for (SceneNodeIndex i(0); i < scene.NumNodes(); ++i) { + DRACO_RETURN_IF_ERROR(AddSceneNode(scene, i)); + } + // There is 1:1 mapping between draco::Scene node indices and |nodes_|. + for (int i = 0; i < scene.NumRootNodes(); ++i) { + nodes_[scene.GetRootNodeIndex(i).value()].root_node = true; + } + DRACO_RETURN_IF_ERROR(AddAnimations(scene)); + DRACO_RETURN_IF_ERROR(AddSkins(scene)); + DRACO_RETURN_IF_ERROR(AddLights(scene)); + DRACO_RETURN_IF_ERROR(AddMaterialsVariantsNames(scene)); + DRACO_RETURN_IF_ERROR(AddInstanceArrays(scene)); + AddStructuralMetadata(scene); + return OkStatus(); +} + +Status GltfAsset::AddSceneNode(const Scene &scene, + SceneNodeIndex scene_node_index) { + const SceneNode *const scene_node = scene.GetNode(scene_node_index); + if (scene_node == nullptr) { + return Status(Status::DRACO_ERROR, "Could not find node in scene."); + } + + GltfNode node; + node.name = scene_node->GetName(); + node.trs_matrix.Copy(scene_node->GetTrsMatrix()); + + for (int i = 0; i < scene_node->NumChildren(); ++i) { + node.childern_indices.push_back(scene_node->Child(i).value()); + } + + const MeshGroupIndex mesh_group_index = scene_node->GetMeshGroupIndex(); + if (mesh_group_index != kInvalidMeshGroupIndex) { + const auto it = mesh_group_index_to_gltf_mesh_.find(mesh_group_index); + if (it == mesh_group_index_to_gltf_mesh_.end()) { + GltfMesh gltf_mesh; + const MeshGroup *const mesh_group = scene.GetMeshGroup(mesh_group_index); + if (!mesh_group->GetName().empty()) { + gltf_mesh.name = mesh_group->GetName(); + } + meshes_.push_back(gltf_mesh); + + for (int i = 0; i < mesh_group->NumMeshInstances(); ++i) { + const MeshGroup::MeshInstance &instance = + mesh_group->GetMeshInstance(i); + const auto mi_it = + mesh_index_to_gltf_mesh_primitive_.find(instance.mesh_index); + if (mi_it == mesh_index_to_gltf_mesh_primitive_.end()) { + // We have not added the mesh to the scene yet. + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + if (!AddDracoMesh(mesh, instance.material_index, + instance.materials_variants_mappings, + base_mesh_transforms_[instance.mesh_index])) { + return Status(Status::DRACO_ERROR, "Adding a Draco mesh failed."); + } + const int gltf_mesh_index = meshes_.size() - 1; + const int gltf_primitive_index = meshes_.back().primitives.size() - 1; + mesh_index_to_gltf_mesh_primitive_[instance.mesh_index] = + std::make_pair(gltf_mesh_index, gltf_primitive_index); + } else { + // The mesh was already added to the scene. This is a copy instance + // that may have a different material. + const int gltf_mesh_index = mi_it->second.first; + const int gltf_primitive_index = mi_it->second.second; + GltfPrimitive primitive = + meshes_[gltf_mesh_index].primitives[gltf_primitive_index]; + primitive.material = instance.material_index; + primitive.material_variants_mappings = + instance.materials_variants_mappings; + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + primitive.mesh_features.clear(); + primitive.mesh_features.reserve(mesh.NumMeshFeatures()); + for (MeshFeaturesIndex j(0); j < mesh.NumMeshFeatures(); ++j) { + primitive.mesh_features.push_back(&mesh.GetMeshFeatures(j)); + } + meshes_.back().primitives.push_back(primitive); + } + } + mesh_group_index_to_gltf_mesh_[mesh_group_index] = meshes_.size() - 1; + } + node.mesh_index = mesh_group_index_to_gltf_mesh_[mesh_group_index]; + } + node.skin_index = scene_node->GetSkinIndex().value(); + node.light_index = scene_node->GetLightIndex().value(); + node.instance_array_index = scene_node->GetInstanceArrayIndex().value(); + + nodes_.push_back(node); + return OkStatus(); +} + +bool GltfAsset::AddMaterials(const Scene &scene) { + if (scene.GetMaterialLibrary().NumMaterials() == 0) { + return true; + } + material_library_.Copy(scene.GetMaterialLibrary()); + return true; +} + +Status GltfAsset::AddAnimations(const Scene &scene) { + if (scene.NumAnimations() == 0) { + return OkStatus(); + } + // Mapping of the node animation data to the output accessors. The first part + // of the key is the animation index and the second part of the key is the + // node animation data index. + std::map, int> node_animation_data_to_accessor; + + // Mapping of the node animation data to the output accessors. + std::unordered_map + data_to_index_map; + + // First add all the accessors and create a mapping from animation accessors + // to accessors owned by the encoder. + for (AnimationIndex i(0); i < scene.NumAnimations(); ++i) { + const Animation *const animation = scene.GetAnimation(i); + + for (int j = 0; j < animation->NumNodeAnimationData(); ++j) { + const NodeAnimationData *const node_animation_data = + animation->GetNodeAnimationData(j); + + int index = -1; + + NodeAnimationDataHash nadh(node_animation_data); + if (data_to_index_map.find(nadh) == data_to_index_map.end()) { + // The current data is new, add it to the encoder. + DRACO_ASSIGN_OR_RETURN( + index, AddNodeAnimationData(*nadh.GetNodeAnimationData())); + data_to_index_map[nadh] = index; + } else { + index = data_to_index_map[nadh]; + } + + const auto key = std::make_pair(i.value(), j); + node_animation_data_to_accessor[key] = index; + } + } + + // Add all the samplers and channels. + for (AnimationIndex i(0); i < scene.NumAnimations(); ++i) { + const Animation *const animation = scene.GetAnimation(i); + std::unique_ptr new_animation(new EncoderAnimation); + new_animation->name = animation->GetName(); + + for (int j = 0; j < animation->NumSamplers(); ++j) { + const AnimationSampler *const sampler = animation->GetSampler(j); + const auto input_key = std::make_pair(i.value(), sampler->input_index); + const auto input_it = node_animation_data_to_accessor.find(input_key); + if (input_it == node_animation_data_to_accessor.end()) { + return Status(Status::DRACO_ERROR, + "Could not find animation accessor input index."); + } + const auto output_key = std::make_pair(i.value(), sampler->output_index); + const auto output_it = node_animation_data_to_accessor.find(output_key); + if (output_it == node_animation_data_to_accessor.end()) { + return Status(Status::DRACO_ERROR, + "Could not find animation accessor output index."); + } + + std::unique_ptr new_sampler(new AnimationSampler()); + new_sampler->input_index = input_it->second; + new_sampler->output_index = output_it->second; + + if (output_type_ == GltfEncoder::COMPACT) { + // Remove min/max from output accessor. + accessors_[new_sampler->output_index].min.clear(); + accessors_[new_sampler->output_index].max.clear(); + } + + new_sampler->interpolation_type = sampler->interpolation_type; + + new_animation->samplers.push_back(std::move(new_sampler)); + } + + for (int j = 0; j < animation->NumChannels(); ++j) { + const AnimationChannel *const channel = animation->GetChannel(j); + std::unique_ptr new_channel(new AnimationChannel()); + new_channel->Copy(*channel); + new_animation->channels.push_back(std::move(new_channel)); + } + + animations_.push_back(std::move(new_animation)); + } + return OkStatus(); +} + +StatusOr GltfAsset::AddNodeAnimationData( + const NodeAnimationData &node_animation_data) { + const size_t buffer_start_offset = buffer_.size(); + + const int component_size = node_animation_data.ComponentSize(); + const int num_components = node_animation_data.NumComponents(); + const std::vector *data = node_animation_data.GetData(); + + std::vector min_values; + min_values.resize(num_components); + for (int j = 0; j < num_components; ++j) { + min_values[j] = (*data)[j]; + } + std::vector max_values = min_values; + + for (int i = 0; i < node_animation_data.count(); ++i) { + for (int j = 0; j < num_components; ++j) { + const float value = (*data)[(i * num_components) + j]; + if (value < min_values[j]) { + min_values[j] = value; + } + if (value > max_values[j]) { + max_values[j] = value; + } + + buffer_.Encode(&value, component_size); + } + } + + if (!PadBuffer()) { + return Status(Status::DRACO_ERROR, + "AddNodeAnimationData: PadBuffer returned DRACO_ERROR."); + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + + GltfAccessor accessor; + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + accessor.component_type = ComponentType::FLOAT; + accessor.count = node_animation_data.count(); + for (int j = 0; j < num_components; ++j) { + accessor.max.push_back(GltfValue(max_values[j])); + accessor.min.push_back(GltfValue(min_values[j])); + } + accessor.type = node_animation_data.TypeAsString(); + accessor.normalized = node_animation_data.normalized(); + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +Status GltfAsset::AddSkins(const Scene &scene) { + if (scene.NumSkins() == 0) { + return OkStatus(); + } + + for (SkinIndex i(0); i < scene.NumSkins(); ++i) { + const Skin *const skin = scene.GetSkin(i); + DRACO_ASSIGN_OR_RETURN( + const int output_accessor_index, + AddNodeAnimationData(skin->GetInverseBindMatrices())); + + std::unique_ptr encoder_skin(new EncoderSkin); + encoder_skin->inverse_bind_matrices_index = output_accessor_index; + encoder_skin->joints.reserve(skin->NumJoints()); + for (int j = 0; j < skin->NumJoints(); j++) { + encoder_skin->joints.push_back(skin->GetJoint(j).value()); + } + encoder_skin->skeleton_index = skin->GetJointRoot().value(); + skins_.push_back(std::move(encoder_skin)); + } + return OkStatus(); +} + +Status GltfAsset::AddLights(const Scene &scene) { + if (scene.NumLights() == 0) { + return OkStatus(); + } + + for (LightIndex i(0); i < scene.NumLights(); ++i) { + std::unique_ptr light = std::unique_ptr(new Light()); + light->Copy(*scene.GetLight(i)); + lights_.push_back(std::move(light)); + } + return OkStatus(); +} + +Status GltfAsset::AddMaterialsVariantsNames(const Scene &scene) { + const MaterialLibrary &library = scene.GetMaterialLibrary(); + for (int i = 0; i < library.NumMaterialsVariants(); ++i) { + materials_variants_names_.push_back(library.GetMaterialsVariantName(i)); + } + return OkStatus(); +} + +Status GltfAsset::AddInstanceArrays(const Scene &scene) { + if (scene.NumInstanceArrays() == 0) { + return OkStatus(); + } + + // Add each of the instance arrays. + std::vector t_data; + std::vector r_data; + std::vector s_data; + for (InstanceArrayIndex i(0); i < scene.NumInstanceArrays(); ++i) { + // Find which of the optional TRS components are set. + // TODO(vytyaz): Treat default TRS component vectors as absent. + const InstanceArray &array = *scene.GetInstanceArray(i); + bool is_t_set = false; + bool is_r_set = false; + bool is_s_set = false; + for (int i = 0; i < array.NumInstances(); i++) { + const InstanceArray::Instance &instance = array.GetInstance(i); + if (instance.trs.TranslationSet()) { + is_t_set = true; + } + if (instance.trs.RotationSet()) { + is_r_set = true; + } + if (instance.trs.ScaleSet()) { + is_s_set = true; + } + } + + // Create contiguous data vectors for individual TRS components. + t_data.clear(); + r_data.clear(); + s_data.clear(); + if (is_t_set) { + t_data.reserve(array.NumInstances() * 3); + } + if (is_r_set) { + r_data.reserve(array.NumInstances() * 4); + } + if (is_s_set) { + s_data.reserve(array.NumInstances() * 3); + } + + // Add TRS vectors of each instance to corresponding data vectors. + for (int i = 0; i < array.NumInstances(); i++) { + const InstanceArray::Instance &instance = array.GetInstance(i); + if (is_t_set) { + DRACO_ASSIGN_OR_RETURN(const auto &t_vector, + instance.trs.Translation()); + t_data.push_back(t_vector.x()); + t_data.push_back(t_vector.y()); + t_data.push_back(t_vector.z()); + } + if (is_r_set) { + DRACO_ASSIGN_OR_RETURN(const auto &r_vector, instance.trs.Rotation()); + r_data.push_back(r_vector.x()); + r_data.push_back(r_vector.y()); + r_data.push_back(r_vector.z()); + r_data.push_back(r_vector.w()); + } + if (is_s_set) { + DRACO_ASSIGN_OR_RETURN(const auto &s_vector, instance.trs.Scale()); + s_data.push_back(s_vector.x()); + s_data.push_back(s_vector.y()); + s_data.push_back(s_vector.z()); + } + } + + // Add TRS vectors to attribute buffers and collect their accessor indices. + EncoderInstanceArray accessors; + if (is_t_set) { + DRACO_ASSIGN_OR_RETURN(accessors.translation, AddData(t_data, 3)); + } + if (is_r_set) { + DRACO_ASSIGN_OR_RETURN(accessors.rotation, AddData(r_data, 4)); + } + if (is_s_set) { + DRACO_ASSIGN_OR_RETURN(accessors.scale, AddData(s_data, 3)); + } + + // Store accessors for later to encode as EXT_mesh_gpu_instancing extension. + instance_arrays_.push_back(accessors); + } + return OkStatus(); +} + +template +void GltfAsset::AddStructuralMetadata(const GeometryT &geometry) { + const StructuralMetadata &structural_metadata = + geometry.GetStructuralMetadata(); + if (!structural_metadata.GetPropertyTableSchema().Empty()) { + property_table_schema_ = structural_metadata.GetPropertyTableSchema(); + for (int i = 0; i < structural_metadata.NumPropertyTables(); ++i) { + property_tables_.push_back(&structural_metadata.GetPropertyTable(i)); + } + } +} + +StatusOr GltfAsset::AddData(const std::vector &data, + int num_components) { + std::string type; + switch (num_components) { + case 3: + type = "VEC3"; + break; + case 4: + type = "VEC4"; + break; + default: + return ErrorStatus("Unsupported number of components."); + } + + const size_t buffer_start_offset = buffer_.size(); + + std::vector min_values(num_components); + for (int j = 0; j < num_components; ++j) { + min_values[j] = data[j]; + } + std::vector max_values = min_values; + + const int count = data.size() / num_components; + for (int i = 0; i < count; ++i) { + for (int j = 0; j < num_components; ++j) { + const float value = data[(i * num_components) + j]; + if (value < min_values[j]) { + min_values[j] = value; + } + if (value > max_values[j]) { + max_values[j] = value; + } + buffer_.Encode(&value, sizeof(float)); + } + } + + if (!PadBuffer()) { + return ErrorStatus("AddArray: PadBuffer returned DRACO_ERROR."); + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + + GltfAccessor accessor; + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + accessor.component_type = ComponentType::FLOAT; + accessor.count = count; + for (int j = 0; j < num_components; ++j) { + accessor.max.push_back(GltfValue(max_values[j])); + accessor.min.push_back(GltfValue(min_values[j])); + } + accessor.type = type; + accessor.normalized = false; + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +StatusOr GltfAsset::AddBufferView( + const PropertyTable::Property::Data &data) { + const size_t buffer_start_offset = buffer_.size(); + buffer_.Encode(data.data.data(), data.data.size()); + if (!PadBuffer()) { + return ErrorStatus("AddBufferView: PadBuffer returned DRACO_ERROR."); + } + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_view.target = data.target; + buffer_views_.push_back(buffer_view); + return static_cast(buffer_views_.size() - 1); +} + +bool GltfAsset::EncodeAssetProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginObject("asset"); + gltf_json_.OutputValue("version", version_); + gltf_json_.OutputValue("generator", generator_); + gltf_json_.EndObject(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeScenesProperty(EncoderBuffer *buf_out) { + // We currently only support one scene. + gltf_json_.BeginArray("scenes"); + gltf_json_.BeginObject(); + gltf_json_.BeginArray("nodes"); + + for (int i = 0; i < nodes_.size(); ++i) { + if (nodes_[i].root_node) { + gltf_json_.OutputValue(i); + } + } + gltf_json_.EndArray(); + gltf_json_.EndObject(); + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeInitialSceneProperty(EncoderBuffer *buf_out) { + gltf_json_.OutputValue("scene", scene_index_); + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeNodesProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("nodes"); + + for (int i = 0; i < nodes_.size(); ++i) { + gltf_json_.BeginObject(); + if (!nodes_[i].name.empty()) { + gltf_json_.OutputValue("name", nodes_[i].name); + } + if (nodes_[i].mesh_index >= 0) { + gltf_json_.OutputValue("mesh", nodes_[i].mesh_index); + } + if (nodes_[i].skin_index >= 0) { + gltf_json_.OutputValue("skin", nodes_[i].skin_index); + } + if (nodes_[i].instance_array_index >= 0 || nodes_[i].light_index >= 0) { + gltf_json_.BeginObject("extensions"); + if (nodes_[i].instance_array_index >= 0) { + gltf_json_.BeginObject("EXT_mesh_gpu_instancing"); + gltf_json_.BeginObject("attributes"); + const int index = nodes_[i].instance_array_index; + const EncoderInstanceArray &accessors = instance_arrays_[index]; + if (accessors.translation != -1) { + gltf_json_.OutputValue("TRANSLATION", accessors.translation); + } + if (accessors.rotation != -1) { + gltf_json_.OutputValue("ROTATION", accessors.rotation); + } + if (accessors.scale != -1) { + gltf_json_.OutputValue("SCALE", accessors.scale); + } + gltf_json_.EndObject(); + gltf_json_.EndObject(); + } + if (nodes_[i].light_index >= 0) { + gltf_json_.BeginObject("KHR_lights_punctual"); + gltf_json_.OutputValue("light", nodes_[i].light_index); + gltf_json_.EndObject(); + } + gltf_json_.EndObject(); + } + + if (!nodes_[i].childern_indices.empty()) { + gltf_json_.BeginArray("children"); + for (int j = 0; j < nodes_[i].childern_indices.size(); ++j) { + gltf_json_.OutputValue(nodes_[i].childern_indices[j]); + } + gltf_json_.EndArray(); + } + + if (!nodes_[i].trs_matrix.IsMatrixIdentity()) { + const auto maybe_transformation = nodes_[i].trs_matrix.Matrix(); + const auto transformation = maybe_transformation.ValueOrDie(); + + if (nodes_[i].trs_matrix.IsMatrixTranslationOnly()) { + gltf_json_.BeginArray("translation"); + for (int j = 0; j < 3; ++j) { + gltf_json_.OutputValue(transformation(j, 3)); + } + gltf_json_.EndArray(); + } else { + gltf_json_.BeginArray("matrix"); + for (int j = 0; j < 4; ++j) { + for (int k = 0; k < 4; ++k) { + gltf_json_.OutputValue(transformation(k, j)); + } + } + gltf_json_.EndArray(); + } + } else { + if (nodes_[i].trs_matrix.TranslationSet()) { + const auto maybe_translation = nodes_[i].trs_matrix.Translation(); + const auto translation = maybe_translation.ValueOrDie(); + gltf_json_.BeginArray("translation"); + for (int j = 0; j < 3; ++j) { + gltf_json_.OutputValue(translation[j]); + } + gltf_json_.EndArray(); + } + if (nodes_[i].trs_matrix.RotationSet()) { + const auto maybe_rotation = nodes_[i].trs_matrix.Rotation(); + const auto rotation = maybe_rotation.ValueOrDie(); + gltf_json_.BeginArray("rotation"); + for (int j = 0; j < 4; ++j) { + // Note: coeffs() returns quaternion values as (x, y, z, w) which is + // the expected format of glTF. + gltf_json_.OutputValue(rotation.coeffs()[j]); + } + gltf_json_.EndArray(); + } + if (nodes_[i].trs_matrix.ScaleSet()) { + const auto maybe_scale = nodes_[i].trs_matrix.Scale(); + const auto scale = maybe_scale.ValueOrDie(); + gltf_json_.BeginArray("scale"); + for (int j = 0; j < 3; ++j) { + gltf_json_.OutputValue(scale[j]); + } + gltf_json_.EndArray(); + } + } + + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +Status GltfAsset::EncodeMeshesProperty(EncoderBuffer *buf_out) { + mesh_features_texture_index_ = 0; + gltf_json_.BeginArray("meshes"); + + for (int i = 0; i < meshes_.size(); ++i) { + gltf_json_.BeginObject(); + + if (!meshes_[i].name.empty()) { + gltf_json_.OutputValue("name", meshes_[i].name); + } + + if (!meshes_[i].primitives.empty()) { + gltf_json_.BeginArray("primitives"); + + for (int j = 0; j < meshes_[i].primitives.size(); ++j) { + const GltfPrimitive &primitive = meshes_[i].primitives[j]; + gltf_json_.BeginObject(); + + gltf_json_.BeginObject("attributes"); + for (auto const &it : primitive.attributes) { + gltf_json_.OutputValue(it.first, it.second); + } + gltf_json_.EndObject(); + + if (primitive.indices >= 0) { + gltf_json_.OutputValue("indices", primitive.indices); + } + gltf_json_.OutputValue("mode", primitive.mode); + if (primitive.material >= 0) { + gltf_json_.OutputValue("material", primitive.material); + } + DRACO_RETURN_IF_ERROR( + EncodePrimitiveExtensionsProperty(primitive, buf_out)); + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + } + + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return ErrorStatus("Failed encoding meshes."); + } + return OkStatus(); +} + +Status GltfAsset::EncodePrimitiveExtensionsProperty( + const GltfPrimitive &primitive, EncoderBuffer *buf_out) { + // Return if the primitive has no extensions to encode. + const bool has_draco_mesh_compression = + primitive.compressed_mesh_info.buffer_view_index >= 0; + const bool has_materials_variants = + !primitive.material_variants_mappings.empty(); + const bool has_mesh_features = !primitive.mesh_features.empty(); + if (!has_draco_mesh_compression && !has_materials_variants && + !has_mesh_features) { + return OkStatus(); + } + + // Encode primitive extensions. + gltf_json_.BeginObject("extensions"); + if (has_draco_mesh_compression) { + gltf_json_.BeginObject("KHR_draco_mesh_compression"); + gltf_json_.OutputValue("bufferView", + primitive.compressed_mesh_info.buffer_view_index); + gltf_json_.BeginObject("attributes"); + for (auto const &it : primitive.compressed_mesh_info.attributes) { + gltf_json_.OutputValue(it.first, it.second); + } + gltf_json_.EndObject(); // attributes entry. + gltf_json_.EndObject(); // KHR_draco_mesh_compression entry. + } + if (has_materials_variants) { + gltf_json_.BeginObject("KHR_materials_variants"); + gltf_json_.BeginArray("mappings"); + for (const auto &mapping : primitive.material_variants_mappings) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("material", mapping.material); + gltf_json_.BeginArray("variants"); + for (const int variant : mapping.variants) { + gltf_json_.OutputValue(variant); + } + gltf_json_.EndArray(); // variants array. + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); // mappings array. + gltf_json_.EndObject(); // KHR_materials_variants entry. + } + if (has_mesh_features) { + gltf_json_.BeginObject("EXT_mesh_features"); + gltf_json_.BeginArray("featureIds"); + for (int i = 0; i < primitive.mesh_features.size(); i++) { + const auto &features = primitive.mesh_features[i]; + gltf_json_.BeginObject(); + if (!features->GetLabel().empty()) { + gltf_json_.OutputValue("label", features->GetLabel()); + } + gltf_json_.OutputValue("featureCount", features->GetFeatureCount()); + if (features->GetAttributeIndex() != -1) { + gltf_json_.OutputValue("attribute", features->GetAttributeIndex()); + } + if (features->GetPropertyTableIndex() != -1) { + gltf_json_.OutputValue("propertyTable", + features->GetPropertyTableIndex()); + } + if (features->GetTextureMap().tex_coord_index() != -1) { + const TextureMap &texture_map = features->GetTextureMap(); + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *texture_map.texture(), mesh_features_texture_index_++, + "_MeshFeatures"); + + // Save image as RGBA if the A channel is used to store feature ID. + const auto &channels = features->GetTextureChannels(); + const int num_channels = + std::count(channels.begin(), channels.end(), 3) == 1 ? 4 : 3; + DRACO_ASSIGN_OR_RETURN( + const int image_index, + AddImage(texture_stem, texture_map.texture(), num_channels)); + const int tex_coord_index = texture_map.tex_coord_index(); + Material dummy_material; + DRACO_RETURN_IF_ERROR(EncodeTextureMap("texture", image_index, + tex_coord_index, dummy_material, + texture_map, channels)); + } + if (features->GetNullFeatureId() != -1) { + gltf_json_.OutputValue("nullFeatureId", features->GetNullFeatureId()); + } + gltf_json_.EndObject(); + mesh_features_used_ = true; + } + gltf_json_.EndArray(); // featureIds array. + gltf_json_.EndObject(); // EXT_mesh_features entry. + } + gltf_json_.EndObject(); // extensions entry. + return OkStatus(); +} + +Status GltfAsset::EncodeMaterials(EncoderBuffer *buf_out) { + // Check if we have textures to write. + if (material_library_.NumMaterials() == 0) { + return EncodeDefaultMaterial(buf_out); + } + return EncodeMaterialsProperty(buf_out); +} + +void GltfAsset::EncodeColorMaterial(float red, float green, float blue, + float alpha, float metallic_factor) { + gltf_json_.BeginObject("pbrMetallicRoughness"); + + gltf_json_.BeginArray("baseColorFactor"); + gltf_json_.OutputValue(red); + gltf_json_.OutputValue(green); + gltf_json_.OutputValue(blue); + gltf_json_.OutputValue(alpha); + gltf_json_.EndArray(); + gltf_json_.OutputValue("metallicFactor", metallic_factor); + + gltf_json_.EndObject(); // pbrMetallicRoughness +} + +Status GltfAsset::EncodeDefaultMaterial(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("materials"); + gltf_json_.BeginObject(); + EncodeColorMaterial(0.75, 0.75, 0.75, 1.0, 0.0); + gltf_json_.EndObject(); + gltf_json_.EndArray(); // materials + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Error encoding default material."); + } + return OkStatus(); +} + +Status GltfAsset::EncodeTextureMap(const std::string &object_name, + int image_index, int tex_coord_index, + const Material &material, + const TextureMap &texture_map) { + return EncodeTextureMap(object_name, image_index, tex_coord_index, material, + texture_map, {}); +} + +Status GltfAsset::EncodeTextureMap(const std::string &object_name, + int image_index, int tex_coord_index, + const Material &material, + const TextureMap &texture_map, + const std::vector &channels) { + // Create a new texture sampler (or reuse an existing one if possible). + const TextureSampler sampler(texture_map.min_filter(), + texture_map.mag_filter(), + texture_map.wrapping_mode()); + DRACO_ASSIGN_OR_RETURN(const int sampler_index, AddTextureSampler(sampler)); + + // Check if we can reuse an existing texture object. + const GltfTexture texture(image_index, sampler_index); + const auto texture_it = + std::find(textures_.begin(), textures_.end(), texture); + int texture_index; + if (texture_it == textures_.end()) { + // Create a new texture object for this texture map. + texture_index = textures_.size(); + textures_.push_back(GltfTexture(image_index, sampler_index)); + } else { + // Reuse an existing texture object. + texture_index = std::distance(textures_.begin(), texture_it); + } + + gltf_json_.BeginObject(object_name); + gltf_json_.OutputValue("index", texture_index); + gltf_json_.OutputValue("texCoord", tex_coord_index); + if (object_name == "normalTexture") { + const float scale = material.GetNormalTextureScale(); + if (scale != 1.0f) { + gltf_json_.OutputValue("scale", scale); + } + } + + // The "texture" object of the EXT_mesh_features extension has a custom + // property "channels" that is encoded here. + if (object_name == "texture" && !channels.empty()) { + gltf_json_.BeginArray("channels"); + for (const int channel : channels) { + gltf_json_.OutputValue(channel); + } + gltf_json_.EndArray(); // channels array. + } + + // Check if |texture_map| is using the KHR_texture_transform extension. + if (!TextureTransform::IsDefault(texture_map.texture_transform())) { + gltf_json_.BeginObject("extensions"); + gltf_json_.BeginObject("KHR_texture_transform"); + if (texture_map.texture_transform().IsOffsetSet()) { + const std::array &offset = + texture_map.texture_transform().offset(); + gltf_json_.BeginArray("offset"); + gltf_json_.OutputValue(offset[0]); + gltf_json_.OutputValue(offset[1]); + gltf_json_.EndArray(); + } + if (texture_map.texture_transform().IsRotationSet()) { + gltf_json_.OutputValue("rotation", + texture_map.texture_transform().rotation()); + } + if (texture_map.texture_transform().IsScaleSet()) { + const std::array &scale = + texture_map.texture_transform().scale(); + gltf_json_.BeginArray("scale"); + gltf_json_.OutputValue(scale[0]); + gltf_json_.OutputValue(scale[1]); + gltf_json_.EndArray(); + } + // TODO(fgalligan): The spec says the extension is not required if the + // pre-transform and the post-transform tex coords are the same. But I'm not + // sure why. I have filed a bug asking for clarification. + // https://github.com/KhronosGroup/glTF/issues/1724 + if (texture_map.texture_transform().IsTexCoordSet()) { + gltf_json_.OutputValue("texCoord", + texture_map.texture_transform().tex_coord()); + } else { + extensions_required_.insert("KHR_texture_transform"); + } + gltf_json_.EndObject(); + gltf_json_.EndObject(); + + extensions_used_.insert("KHR_texture_transform"); + } + gltf_json_.EndObject(); + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialsProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("materials"); + for (int i = 0; i < material_library_.NumMaterials(); ++i) { + const Material *const material = material_library_.GetMaterial(i); + if (!material) { + return Status(Status::DRACO_ERROR, "Error getting material."); + } + + const TextureMap *const color = + material->GetTextureMapByType(TextureMap::COLOR); + const TextureMap *const metallic = + material->GetTextureMapByType(TextureMap::METALLIC_ROUGHNESS); + const TextureMap *const normal = + material->GetTextureMapByType(TextureMap::NORMAL_TANGENT_SPACE); + const TextureMap *const occlusion = + material->GetTextureMapByType(TextureMap::AMBIENT_OCCLUSION); + const TextureMap *const emissive = + material->GetTextureMapByType(TextureMap::EMISSIVE); + + // Check if material is unlit and does not have a fallback. + if (material->GetUnlit() && + (!color || metallic || normal || occlusion || emissive || + material->GetMetallicFactor() != 0.0 || + material->GetRoughnessFactor() <= 0.5 || + material->GetEmissiveFactor() != Vector3f(0.0, 0.0, 0.0))) { + // If we find one material that is unlit and does not contain a fallback + // we must set "KHR_materials_unlit" in extensions reqruied for the entire + // glTF file. + extensions_required_.insert("KHR_materials_unlit"); + } + + int occlusion_metallic_roughness_image_index = -1; + + gltf_json_.BeginObject(); // material object. + + gltf_json_.BeginObject("pbrMetallicRoughness"); + if (color) { + const bool rgba = true; // Unused for now. + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *color->texture(), i, "_BaseColor"); + DRACO_ASSIGN_OR_RETURN( + const int color_image_index, + AddImage(texture_stem, color->texture(), rgba ? 4 : 3)); + DRACO_RETURN_IF_ERROR( + EncodeTextureMap("baseColorTexture", color_image_index, + color->tex_coord_index(), *material, *color)); + } + // Try to combine metallic and occlusion only if they have the same tex + // coord index. + // TODO(b/145991271): Check out if we need to check texture indices. + if (metallic && occlusion && + metallic->tex_coord_index() == occlusion->tex_coord_index()) { + if (metallic->texture() == occlusion->texture()) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *metallic->texture(), i, "_OcclusionMetallicRoughness"); + // Metallic and occlusion textures are already combined. + DRACO_ASSIGN_OR_RETURN(occlusion_metallic_roughness_image_index, + AddImage(texture_stem, metallic->texture(), 3)); + } + if (occlusion_metallic_roughness_image_index != -1) + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "metallicRoughnessTexture", + occlusion_metallic_roughness_image_index, + metallic->tex_coord_index(), *material, *metallic)); + } + + if (metallic && occlusion_metallic_roughness_image_index == -1) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *metallic->texture(), i, "_MetallicRoughness"); + DRACO_ASSIGN_OR_RETURN(const int metallic_roughness_image_index, + AddImage(texture_stem, metallic->texture(), 3)); + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "metallicRoughnessTexture", metallic_roughness_image_index, + metallic->tex_coord_index(), *material, *metallic)); + } + + EncodeVectorArray("baseColorFactor", material->GetColorFactor()); + gltf_json_.OutputValue("metallicFactor", material->GetMetallicFactor()); + gltf_json_.OutputValue("roughnessFactor", material->GetRoughnessFactor()); + gltf_json_.EndObject(); // pbrMetallicRoughness + + if (normal) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *normal->texture(), i, "_Normal"); + DRACO_ASSIGN_OR_RETURN(const int normal_image_index, + AddImage(texture_stem, normal->texture(), 3)); + DRACO_RETURN_IF_ERROR( + EncodeTextureMap("normalTexture", normal_image_index, + normal->tex_coord_index(), *material, *normal)); + } + + if (occlusion_metallic_roughness_image_index != -1) { + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "occlusionTexture", occlusion_metallic_roughness_image_index, + metallic->tex_coord_index(), *material, *metallic)); + } else if (occlusion) { + // Store occlusion texture in a grayscale format, unless it is used by + // metallic-roughness map of some other matierial. It is possible that + // this material uses occlusion (R channel) and some other material uses + // metallic-roughness (GB channels) from this texture. + const int num_components = TextureUtils::ComputeRequiredNumChannels( + *occlusion->texture(), material_library_); + const std::string suffix = + (num_components == 1) ? "_Occlusion" : "_OcclusionMetallicRoughness"; + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *occlusion->texture(), i, suffix); + DRACO_ASSIGN_OR_RETURN( + const int occlusion_image_index, + AddImage(texture_stem, occlusion->texture(), num_components)); + DRACO_RETURN_IF_ERROR(EncodeTextureMap( + "occlusionTexture", occlusion_image_index, + occlusion->tex_coord_index(), *material, *occlusion)); + } + + if (emissive) { + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *emissive->texture(), i, "_Emissive"); + DRACO_ASSIGN_OR_RETURN(const int emissive_image_index, + AddImage(texture_stem, emissive->texture(), 3)); + DRACO_RETURN_IF_ERROR( + EncodeTextureMap("emissiveTexture", emissive_image_index, + emissive->tex_coord_index(), *material, *emissive)); + } + + EncodeVectorArray("emissiveFactor", + material->GetEmissiveFactor()); + + switch (material->GetTransparencyMode()) { + case Material::TransparencyMode::TRANSPARENCY_MASK: + gltf_json_.OutputValue("alphaMode", "MASK"); + gltf_json_.OutputValue("alphaCutoff", material->GetAlphaCutoff()); + break; + case Material::TransparencyMode::TRANSPARENCY_BLEND: + gltf_json_.OutputValue("alphaMode", "BLEND"); + break; + default: + gltf_json_.OutputValue("alphaMode", "OPAQUE"); + break; + } + if (!material->GetName().empty()) { + gltf_json_.OutputValue("name", material->GetName()); + } + + // Output doubleSided if different than the default. + if (material->GetDoubleSided()) { + gltf_json_.OutputValue("doubleSided", material->GetDoubleSided()); + } + + // Encode material extensions if any. + if (material->GetUnlit() || material->HasSheen() || + material->HasTransmission() || material->HasClearcoat() || + material->HasVolume() || material->HasIor() || + material->HasSpecular()) { + gltf_json_.BeginObject("extensions"); + + // Encode individual material extensions. + if (material->GetUnlit()) { + EncodeMaterialUnlitExtension(*material); + } else { + // PBR extensions can only be added to non-unlit materials. + Material defaults; + if (material->HasSheen()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialSheenExtension(*material, defaults, i)); + } + if (material->HasTransmission()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialTransmissionExtension(*material, defaults, i)); + } + if (material->HasClearcoat()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialClearcoatExtension(*material, defaults, i)); + } + if (material->HasVolume()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialVolumeExtension(*material, defaults, i)); + } + if (material->HasIor()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialIorExtension(*material, defaults)); + } + if (material->HasSpecular()) { + DRACO_RETURN_IF_ERROR( + EncodeMaterialSpecularExtension(*material, defaults, i)); + } + } + + gltf_json_.EndObject(); // extensions object. + } + + gltf_json_.EndObject(); // material object. + } + + gltf_json_.EndArray(); // materials array. + + if (!textures_.empty()) { + gltf_json_.BeginArray("textures"); + for (int i = 0; i < textures_.size(); ++i) { + const int image_index = textures_[i].image_index; + gltf_json_.BeginObject(); + if (images_[image_index].mime_type == "image/webp") { + gltf_json_.BeginObject("extensions"); + gltf_json_.BeginObject("EXT_texture_webp"); + gltf_json_.OutputValue("source", image_index); + gltf_json_.EndObject(); + gltf_json_.EndObject(); + } else if (images_[image_index].mime_type == "image/ktx2") { + gltf_json_.BeginObject("extensions"); + gltf_json_.BeginObject("KHR_texture_basisu"); + gltf_json_.OutputValue("source", image_index); + gltf_json_.EndObject(); + gltf_json_.EndObject(); + } else { + gltf_json_.OutputValue("source", image_index); + } + if (textures_[i].sampler_index >= 0) { + gltf_json_.OutputValue("sampler", textures_[i].sampler_index); + } + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + } + + if (!texture_samplers_.empty()) { + gltf_json_.BeginArray("samplers"); + for (int i = 0; i < texture_samplers_.size(); ++i) { + gltf_json_.BeginObject(); + + const int mode_s = TextureAxisWrappingModeToGltfValue( + texture_samplers_[i].wrapping_mode.s); + const int mode_t = TextureAxisWrappingModeToGltfValue( + texture_samplers_[i].wrapping_mode.t); + gltf_json_.OutputValue("wrapS", mode_s); + gltf_json_.OutputValue("wrapT", mode_t); + + if (texture_samplers_[i].min_filter != TextureMap::UNSPECIFIED) { + gltf_json_.OutputValue( + "minFilter", + TextureFilterTypeToGltfValue(texture_samplers_[i].min_filter)); + } + if (texture_samplers_[i].mag_filter != TextureMap::UNSPECIFIED) { + gltf_json_.OutputValue( + "magFilter", + TextureFilterTypeToGltfValue(texture_samplers_[i].mag_filter)); + } + + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + } + + if (!images_.empty()) { + gltf_json_.BeginArray("images"); + for (int i = 0; i < images_.size(); ++i) { + if (add_images_to_buffer_) { + DRACO_RETURN_IF_ERROR(SaveImageToBuffer(i)); + } + gltf_json_.BeginObject(); + if (images_[i].buffer_view >= 0) { + gltf_json_.OutputValue("bufferView", images_[i].buffer_view); + gltf_json_.OutputValue("mimeType", images_[i].mime_type); + } else { + gltf_json_.OutputValue("uri", images_[i].image_name); + } + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + } + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Error encoding materials."); + } + return OkStatus(); +} + +void GltfAsset::EncodeMaterialUnlitExtension(const Material &material) { + extensions_used_.insert("KHR_materials_unlit"); + gltf_json_.BeginObject("KHR_materials_unlit"); + gltf_json_.EndObject(); +} + +Status GltfAsset::EncodeMaterialSheenExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_sheen"); + gltf_json_.BeginObject("KHR_materials_sheen"); + + // Add sheen color factor, unless it is the default. + if (material.GetSheenColorFactor() != defaults.GetSheenColorFactor()) { + EncodeVectorArray("sheenColorFactor", + material.GetSheenColorFactor()); + } + + // Add sheen roughness factor, unless it is the default. + if (material.GetSheenRoughnessFactor() != + defaults.GetSheenRoughnessFactor()) { + gltf_json_.OutputValue("sheenRoughnessFactor", + material.GetSheenRoughnessFactor()); + } + + // Add sheen color texture (RGB channels) if present. + // TODO(vytyaz): Combine sheen color and roughness images if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("sheenColorTexture", "_SheenColor", + TextureMap::SHEEN_COLOR, -1, material, + material_index)); + + // Add sheen roughness texture (A channel) if present. + DRACO_RETURN_IF_ERROR( + EncodeTexture("sheenRoughnessTexture", "_SheenRoughness", + TextureMap::SHEEN_ROUGHNESS, 4, material, material_index)); + + gltf_json_.EndObject(); // KHR_materials_sheen object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialTransmissionExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_transmission"); + gltf_json_.BeginObject("KHR_materials_transmission"); + + // Add transmission factor, unless it is the default. + if (material.GetTransmissionFactor() != defaults.GetTransmissionFactor()) { + gltf_json_.OutputValue("transmissionFactor", + material.GetTransmissionFactor()); + } + + // Add transmission texture (R channel) if present. + // TODO(vytyaz): Store texture in a grayscale format if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("transmissionTexture", "_Transmission", + TextureMap::TRANSMISSION, 3, material, + material_index)); + + gltf_json_.EndObject(); // KHR_materials_transmission object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialClearcoatExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_clearcoat"); + gltf_json_.BeginObject("KHR_materials_clearcoat"); + + // Add clearcoat factor, unless it is the default. + if (material.GetClearcoatFactor() != defaults.GetClearcoatFactor()) { + gltf_json_.OutputValue("clearcoatFactor", material.GetClearcoatFactor()); + } + + // Add clearcoat roughness factor, unless it is the default. + if (material.GetClearcoatRoughnessFactor() != + defaults.GetClearcoatRoughnessFactor()) { + gltf_json_.OutputValue("clearcoatRoughnessFactor", + material.GetClearcoatRoughnessFactor()); + } + + // Add clearcoat texture (R channel) if present. + // TODO(vytyaz): Combine clearcoat and clearcoat roughness images if possible. + // TODO(vytyaz): Store texture in a grayscale format if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("clearcoatTexture", "_Clearcoat", + TextureMap::CLEARCOAT, 3, material, + material_index)); + + // Add clearcoat roughness texture (G channel) if present. + DRACO_RETURN_IF_ERROR(EncodeTexture( + "clearcoatRoughnessTexture", "_ClearcoatRoughness", + TextureMap::CLEARCOAT_ROUGHNESS, 3, material, material_index)); + + // Add clearcoat normal texture (RGB channels) if present. + DRACO_RETURN_IF_ERROR( + EncodeTexture("clearcoatNormalTexture", "_ClearcoatNormal", + TextureMap::CLEARCOAT_NORMAL, 3, material, material_index)); + + gltf_json_.EndObject(); // KHR_materials_clearcoat object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialVolumeExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_volume"); + gltf_json_.BeginObject("KHR_materials_volume"); + + // Add thickness factor, unless it is the default. + if (material.GetThicknessFactor() != defaults.GetThicknessFactor()) { + gltf_json_.OutputValue("thicknessFactor", material.GetThicknessFactor()); + } + + // Add attenuation distance, unless it is the default. + if (material.GetAttenuationDistance() != defaults.GetAttenuationDistance()) { + gltf_json_.OutputValue("attenuationDistance", + material.GetAttenuationDistance()); + } + + // Add attenuation color, unless it is the default. + if (material.GetAttenuationColor() != defaults.GetAttenuationColor()) { + EncodeVectorArray("attenuationColor", + material.GetAttenuationColor()); + } + + // Add thickness texture (G channel) if present. + DRACO_RETURN_IF_ERROR(EncodeTexture("thicknessTexture", "_Thickness", + TextureMap::THICKNESS, 3, material, + material_index)); + + gltf_json_.EndObject(); // KHR_materials_volume object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialIorExtension(const Material &material, + const Material &defaults) { + extensions_used_.insert("KHR_materials_ior"); + gltf_json_.BeginObject("KHR_materials_ior"); + + // Add ior, unless it is the default. + if (material.GetIor() != defaults.GetIor()) { + gltf_json_.OutputValue("ior", material.GetIor()); + } + + gltf_json_.EndObject(); // KHR_materials_ior object. + + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialSpecularExtension(const Material &material, + const Material &defaults, + int material_index) { + extensions_used_.insert("KHR_materials_specular"); + gltf_json_.BeginObject("KHR_materials_specular"); + + // Add specular factor, unless it is the default. + if (material.GetSpecularFactor() != defaults.GetSpecularFactor()) { + gltf_json_.OutputValue("specularFactor", material.GetSpecularFactor()); + } + + // Add specular color factor, unless it is the default. + if (material.GetSpecularColorFactor() != defaults.GetSpecularColorFactor()) { + EncodeVectorArray("specularColorFactor", + material.GetSpecularColorFactor()); + } + + // Add specular texture (A channel) if present. + // TODO(vytyaz): Combine specular and specular color images if possible. + DRACO_RETURN_IF_ERROR(EncodeTexture("specularTexture", "_Specular", + TextureMap::SPECULAR, 4, material, + material_index)); + + // Add specular color texture (RGB channels) if present. + DRACO_RETURN_IF_ERROR(EncodeTexture("specularColorTexture", "_SpecularColor", + TextureMap::SPECULAR_COLOR, -1, material, + material_index)); + + gltf_json_.EndObject(); // KHR_materials_specular object. + + return OkStatus(); +} + +Status GltfAsset::EncodeTexture(const std::string &name, + const std::string &stem_suffix, + TextureMap::Type type, int num_components, + const Material &material, int material_index) { + const TextureMap *const texture_map = material.GetTextureMapByType(type); + if (texture_map) { + if (num_components == -1) { + const bool rgba = true; // Unused for now. + num_components = rgba ? 4 : 3; + } + const std::string texture_stem = TextureUtils::GetOrGenerateTargetStem( + *texture_map->texture(), material_index, stem_suffix); + DRACO_ASSIGN_OR_RETURN( + const int image_index, + AddImage(texture_stem, texture_map->texture(), num_components)); + DRACO_RETURN_IF_ERROR(EncodeTextureMap(name, image_index, + texture_map->tex_coord_index(), + material, *texture_map)); + } + return OkStatus(); +} + +Status GltfAsset::EncodeAnimationsProperty(EncoderBuffer *buf_out) { + if (animations_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginArray("animations"); + for (int i = 0; i < animations_.size(); ++i) { + gltf_json_.BeginObject(); + + if (!animations_[i]->name.empty()) { + gltf_json_.OutputValue("name", animations_[i]->name); + } + + gltf_json_.BeginArray("samplers"); + for (int j = 0; j < animations_[i]->samplers.size(); ++j) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("input", animations_[i]->samplers[j]->input_index); + gltf_json_.OutputValue( + "interpolation", + AnimationSampler::InterpolationToString( + animations_[i]->samplers[j]->interpolation_type)); + gltf_json_.OutputValue("output", + animations_[i]->samplers[j]->output_index); + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + + gltf_json_.BeginArray("channels"); + for (int j = 0; j < animations_[i]->channels.size(); ++j) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("sampler", + animations_[i]->channels[j]->sampler_index); + + gltf_json_.BeginObject("target"); + gltf_json_.OutputValue("node", animations_[i]->channels[j]->target_index); + gltf_json_.OutputValue( + "path", AnimationChannel::TransformationToString( + animations_[i]->channels[j]->transformation_type)); + gltf_json_.EndObject(); + + gltf_json_.EndObject(); // Channel entry. + } + gltf_json_.EndArray(); + + gltf_json_.EndObject(); // Animmation entry. + } + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Could not encode animations."); + } + return OkStatus(); +} + +Status GltfAsset::EncodeSkinsProperty(EncoderBuffer *buf_out) { + if (skins_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginArray("skins"); + for (int i = 0; i < skins_.size(); ++i) { + gltf_json_.BeginObject(); + + if (skins_[i]->inverse_bind_matrices_index >= 0) { + gltf_json_.OutputValue("inverseBindMatrices", + skins_[i]->inverse_bind_matrices_index); + } + if (skins_[i]->skeleton_index >= 0) { + gltf_json_.OutputValue("skeleton", skins_[i]->skeleton_index); + } + + if (!skins_[i]->joints.empty()) { + gltf_json_.BeginArray("joints"); + for (int j = 0; j < skins_[i]->joints.size(); ++j) { + gltf_json_.OutputValue(skins_[i]->joints[j]); + } + gltf_json_.EndArray(); + } + gltf_json_.EndObject(); // Skin entry. + } + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Could not encode animations."); + } + return OkStatus(); +} + +Status GltfAsset::EncodeTopLevelExtensionsProperty(EncoderBuffer *buf_out) { + // Return if there are no top-level asset extensions to encode. + if (lights_.empty() && materials_variants_names_.empty() && + property_tables_.empty()) { + return OkStatus(); + } + + // Encode top-level extensions. + gltf_json_.BeginObject("extensions"); + DRACO_RETURN_IF_ERROR(EncodeLightsProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeMaterialsVariantsNamesProperty(buf_out)); + DRACO_RETURN_IF_ERROR(EncodeStructuralMetadataProperty(buf_out)); + gltf_json_.EndObject(); // extensions entry. + return OkStatus(); +} + +Status GltfAsset::EncodeLightsProperty(EncoderBuffer *buf_out) { + if (lights_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginObject("KHR_lights_punctual"); + gltf_json_.BeginArray("lights"); + const Light defaults; + for (const auto &light : lights_) { + gltf_json_.BeginObject(); + if (light->GetName() != defaults.GetName()) { + gltf_json_.OutputValue("name", light->GetName()); + } + if (light->GetColor() != defaults.GetColor()) { + gltf_json_.BeginArray("color"); + gltf_json_.OutputValue(light->GetColor()[0]); + gltf_json_.OutputValue(light->GetColor()[1]); + gltf_json_.OutputValue(light->GetColor()[2]); + gltf_json_.EndArray(); + } + if (light->GetIntensity() != defaults.GetIntensity()) { + gltf_json_.OutputValue("intensity", light->GetIntensity()); + } + switch (light->GetType()) { + case Light::DIRECTIONAL: + gltf_json_.OutputValue("type", "directional"); + break; + case Light::POINT: + gltf_json_.OutputValue("type", "point"); + break; + case Light::SPOT: + gltf_json_.OutputValue("type", "spot"); + break; + } + if (light->GetRange() != defaults.GetRange()) { + gltf_json_.OutputValue("range", light->GetRange()); + } + if (light->GetType() == Light::SPOT) { + gltf_json_.BeginObject("spot"); + if (light->GetInnerConeAngle() != defaults.GetInnerConeAngle()) { + gltf_json_.OutputValue("innerConeAngle", light->GetInnerConeAngle()); + } + if (light->GetOuterConeAngle() != defaults.GetOuterConeAngle()) { + gltf_json_.OutputValue("outerConeAngle", light->GetOuterConeAngle()); + } + gltf_json_.EndObject(); + } + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + gltf_json_.EndObject(); // KHR_lights_punctual entry. + return OkStatus(); +} + +Status GltfAsset::EncodeMaterialsVariantsNamesProperty(EncoderBuffer *buf_out) { + if (materials_variants_names_.empty()) { + return OkStatus(); + } + + gltf_json_.BeginObject("KHR_materials_variants"); + gltf_json_.BeginArray("variants"); + for (const std::string &name : materials_variants_names_) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("name", name); + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); + gltf_json_.EndObject(); // KHR_materials_variants entry. + return OkStatus(); +} + +Status GltfAsset::EncodeStructuralMetadataProperty(EncoderBuffer *buf_out) { + if (property_table_schema_.Empty()) { + return OkStatus(); + } + + gltf_json_.BeginObject("EXT_structural_metadata"); + + // Encodes property table schema. + struct SchemaWriter { + static void Write(const PropertyTable::Schema::Object &object, + JsonWriter *json_writer) { + switch (object.GetType()) { + case PropertyTable::Schema::Object::OBJECT: + json_writer->BeginObject(object.GetName()); + for (const PropertyTable::Schema::Object &obj : object.GetObjects()) { + Write(obj, json_writer); + } + json_writer->EndObject(); + break; + case PropertyTable::Schema::Object::ARRAY: + json_writer->BeginArray(object.GetName()); + for (const PropertyTable::Schema::Object &obj : object.GetArray()) { + Write(obj, json_writer); + } + json_writer->EndArray(); + break; + case PropertyTable::Schema::Object::STRING: + json_writer->OutputValue(object.GetName(), object.GetString()); + break; + case PropertyTable::Schema::Object::INTEGER: + json_writer->OutputValue(object.GetName(), object.GetInteger()); + break; + case PropertyTable::Schema::Object::BOOLEAN: + json_writer->OutputValue(object.GetName(), object.GetBoolean()); + break; + } + } + }; + + // Encode property table schema. + SchemaWriter::Write(property_table_schema_.json, &gltf_json_); + + // Encode all property tables. + gltf_json_.BeginArray("propertyTables"); + for (const PropertyTable *const table : property_tables_) { + gltf_json_.BeginObject(); + if (!table->GetName().empty()) { + gltf_json_.OutputValue("name", table->GetName()); + } + if (!table->GetClass().empty()) { + gltf_json_.OutputValue("class", table->GetClass()); + } + gltf_json_.OutputValue("count", table->GetCount()); + + // Encoder all property table properties. + gltf_json_.BeginObject("properties"); + for (int i = 0; i < table->NumProperties(); ++i) { + const PropertyTable::Property &property = table->GetProperty(i); + gltf_json_.BeginObject(property.GetName()); + + // Encode property values. + DRACO_ASSIGN_OR_RETURN(const int buffer_view_index, + AddBufferView(property.GetData())); + gltf_json_.OutputValue("values", buffer_view_index); + + // Encode offsets for variable-length arrays. + if (!property.GetArrayOffsets().data.data.empty()) { + if (!property.GetArrayOffsets().type.empty()) { + gltf_json_.OutputValue("arrayOffsetType", + property.GetArrayOffsets().type); + } + DRACO_ASSIGN_OR_RETURN(const int buffer_view_index, + AddBufferView(property.GetArrayOffsets().data)); + gltf_json_.OutputValue("arrayOffsets", buffer_view_index); + } + + // Encode offsets for strings. + if (!property.GetStringOffsets().data.data.empty()) { + if (!property.GetStringOffsets().type.empty()) { + gltf_json_.OutputValue("stringOffsetType", + property.GetStringOffsets().type); + } + DRACO_ASSIGN_OR_RETURN(const int buffer_view_index, + AddBufferView(property.GetStringOffsets().data)); + gltf_json_.OutputValue("stringOffsets", buffer_view_index); + } + gltf_json_.EndObject(); // Named property entry. + } + gltf_json_.EndObject(); // properties entry. + gltf_json_.EndObject(); + } + gltf_json_.EndArray(); // propertyTables entry. + gltf_json_.EndObject(); // EXT_structural_metadata entry. + return OkStatus(); +} + +bool GltfAsset::EncodeAccessorsProperty(EncoderBuffer *buf_out) { + gltf_json_.BeginArray("accessors"); + + for (int i = 0; i < accessors_.size(); ++i) { + gltf_json_.BeginObject(); + + if (accessors_[i].buffer_view_index >= 0) { + gltf_json_.OutputValue("bufferView", accessors_[i].buffer_view_index); + if (output_type_ == GltfEncoder::VERBOSE) { + gltf_json_.OutputValue("byteOffset", 0); + } + } + gltf_json_.OutputValue("componentType", accessors_[i].component_type); + gltf_json_.OutputValue("count", accessors_[i].count); + if (accessors_[i].normalized) { + gltf_json_.OutputValue("normalized", accessors_[i].normalized); + } + + if (!accessors_[i].max.empty()) { + gltf_json_.BeginArray("max"); + for (int j = 0; j < accessors_[i].max.size(); ++j) { + gltf_json_.OutputValue(accessors_[i].max[j]); + } + gltf_json_.EndArray(); + } + + if (!accessors_[i].min.empty()) { + gltf_json_.BeginArray("min"); + for (int j = 0; j < accessors_[i].min.size(); ++j) { + gltf_json_.OutputValue(accessors_[i].min[j]); + } + gltf_json_.EndArray(); + } + + gltf_json_.OutputValue("type", accessors_[i].type); + + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeBufferViewsProperty(EncoderBuffer *buf_out) { + // We currently only support one buffer. + gltf_json_.BeginArray("bufferViews"); + + for (int i = 0; i < buffer_views_.size(); ++i) { + gltf_json_.BeginObject(); + gltf_json_.OutputValue("buffer", 0); + gltf_json_.OutputValue("byteOffset", buffer_views_[i].buffer_byte_offset); + gltf_json_.OutputValue("byteLength", buffer_views_[i].byte_length); + if (buffer_views_[i].target != 0) { + gltf_json_.OutputValue("target", buffer_views_[i].target); + } + gltf_json_.EndObject(); + } + + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +bool GltfAsset::EncodeBuffersProperty(EncoderBuffer *buf_out) { + if (buffer_.size() == 0) { + return true; + } + // We currently only support one buffer. + gltf_json_.BeginArray("buffers"); + gltf_json_.BeginObject(); + gltf_json_.OutputValue("byteLength", buffer_.size()); + if (!buffer_name_.empty()) { + gltf_json_.OutputValue("uri", buffer_name_); + } + gltf_json_.EndObject(); + gltf_json_.EndArray(); + + const std::string asset_str = gltf_json_.MoveData(); + return buf_out->Encode(asset_str.data(), asset_str.length()); +} + +Status GltfAsset::EncodeExtensionsProperties(EncoderBuffer *buf_out) { + if (draco_compression_used_) { + const std::string draco_tag = "KHR_draco_mesh_compression"; + extensions_used_.insert(draco_tag); + extensions_required_.insert(draco_tag); + } + if (!lights_.empty()) { + extensions_used_.insert("KHR_lights_punctual"); + } + if (!materials_variants_names_.empty()) { + extensions_used_.insert("KHR_materials_variants"); + } + if (!instance_arrays_.empty()) { + extensions_used_.insert("EXT_mesh_gpu_instancing"); + extensions_required_.insert("EXT_mesh_gpu_instancing"); + } + if (mesh_features_used_) { + extensions_used_.insert("EXT_mesh_features"); + } + if (!property_table_schema_.Empty()) { + extensions_used_.insert("EXT_structural_metadata"); + } + + if (!extensions_required_.empty()) { + gltf_json_.BeginArray("extensionsRequired"); + for (const auto &extension : extensions_required_) { + gltf_json_.OutputValue(extension); + } + gltf_json_.EndArray(); + } + if (!extensions_used_.empty()) { + gltf_json_.BeginArray("extensionsUsed"); + for (const auto &extension : extensions_used_) { + gltf_json_.OutputValue(extension); + } + gltf_json_.EndArray(); + } + + const std::string asset_str = gltf_json_.MoveData(); + if (!asset_str.empty()) { + if (!buf_out->Encode(asset_str.data(), asset_str.length())) { + return Status(Status::DRACO_ERROR, "Could not encode extensions."); + } + } + return OkStatus(); +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return BYTE; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return UNSIGNED_BYTE; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return SHORT; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return UNSIGNED_SHORT; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return UNSIGNED_INT; +} + +template <> +GltfAsset::ComponentType GltfAsset::GetComponentType() const { + return FLOAT; +} + +template +int GltfAsset::AddAttribute(const PointAttribute &att, int num_points, + int num_encoded_points, const std::string &type, + bool compress) { + if (att.size() == 0) { + return -1; // Attribute size must be greater than 0. + } + + std::array value; + std::array min_values; + std::array max_values; + + // Set min and max values. + if (!att.ConvertValue(AttributeValueIndex(0), + &min_values[0])) { + return -1; + } + max_values = min_values; + + if (output_type_ == GltfEncoder::VERBOSE || + att.attribute_type() == GeometryAttribute::POSITION) { + for (AttributeValueIndex i(1); i < static_cast(att.size()); ++i) { + if (!att.ConvertValue(i, &value[0])) { + return -1; + } + for (int j = 0; j < att_components_t; ++j) { + if (value[j] < min_values[j]) { + min_values[j] = value[j]; + } + if (value[j] > max_values[j]) { + max_values[j] = value[j]; + } + } + } + } + + const int kComponentSize = sizeof(att_data_t); + + GltfAccessor accessor; + if (!compress) { + const size_t buffer_start_offset = buffer_.size(); + for (PointIndex v(0); v < num_points; ++v) { + if (!att.ConvertValue(att.mapped_index(v), + &value[0])) { + return -1; + } + + for (int j = 0; j < att_components_t; ++j) { + buffer_.Encode(&value[j], kComponentSize); + } + } + + if (!PadBuffer()) { + return -1; + } + + GltfBufferView buffer_view; + buffer_view.buffer_byte_offset = buffer_start_offset; + buffer_view.byte_length = buffer_.size() - buffer_start_offset; + buffer_views_.push_back(buffer_view); + accessor.buffer_view_index = static_cast(buffer_views_.size() - 1); + } + + accessor.component_type = GetComponentType(); + accessor.count = num_encoded_points; + if (output_type_ == GltfEncoder::VERBOSE || + att.attribute_type() == GeometryAttribute::POSITION) { + for (int j = 0; j < att_components_t; ++j) { + accessor.max.push_back(GltfValue(max_values[j])); + accessor.min.push_back(GltfValue(min_values[j])); + } + } + accessor.type = type; + accessor.normalized = att.attribute_type() == GeometryAttribute::COLOR && + att.data_type() != DT_FLOAT32; + accessors_.push_back(accessor); + return static_cast(accessors_.size() - 1); +} + +const char GltfEncoder::kDracoMetadataGltfAttributeName[] = + "//GLTF/ApplicationSpecificAttributeName"; + +GltfEncoder::GltfEncoder() : out_buffer_(nullptr), output_type_(COMPACT) {} + +template +bool GltfEncoder::EncodeToFile(const T &geometry, const std::string &file_name, + const std::string &base_dir) { + const std::string buffer_name = base_dir + "/buffer0.bin"; + return EncodeFile(geometry, file_name, buffer_name, base_dir).ok(); +} + +template +Status GltfEncoder::EncodeFile(const T &geometry, const std::string &filename) { + if (filename.empty()) { + return Status(Status::DRACO_ERROR, "Output parameter is empty."); + } + + std::string dir_path; + std::string basename; + draco::SplitPath(filename, &dir_path, &basename); + const std::string bin_basename = ReplaceFileExtension(basename, "bin"); + const std::string bin_filename = dir_path + "/" + bin_basename; + return EncodeFile(geometry, filename, bin_filename, dir_path); +} + +template +Status GltfEncoder::EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename) { + if (filename.empty()) { + return Status(Status::DRACO_ERROR, "Output parameter is empty."); + } + + std::string dir_path; + std::string basename; + draco::SplitPath(filename, &dir_path, &basename); + return EncodeFile(geometry, filename, bin_filename, dir_path); +} + +template +Status GltfEncoder::EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir) { + if (filename.empty() || bin_filename.empty() || resource_dir.empty()) { + return Status(Status::DRACO_ERROR, "Output parameter is empty."); + } + const std::string extension = LowercaseFileExtension(filename); + if (extension != "gltf" && extension != "glb") { + return Status(Status::DRACO_ERROR, + "gltf_encoder only supports .gltf or .glb output."); + } + + GltfAsset gltf_asset; + gltf_asset.set_output_type(output_type_); + + if (extension == "gltf") { + std::string bin_path; + std::string bin_basename; + draco::SplitPath(bin_filename, &bin_path, &bin_basename); + gltf_asset.buffer_name(bin_basename); + } else { + gltf_asset.buffer_name(""); + gltf_asset.set_add_images_to_buffer(true); + } + + // Encode the geometry into a buffer. + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(EncodeToBuffer(geometry, &gltf_asset, &buffer)); + if (extension == "glb") { + return WriteGlbFile(gltf_asset, buffer, filename); + } + return WriteGltfFiles(gltf_asset, buffer, filename, bin_filename, + resource_dir); +} + +template +Status GltfEncoder::EncodeToBuffer(const T &geometry, + EncoderBuffer *out_buffer) { + GltfAsset gltf_asset; + gltf_asset.set_output_type(output_type_); + gltf_asset.buffer_name(""); + gltf_asset.set_add_images_to_buffer(true); + + // Encode the geometry into a buffer. + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(EncodeToBuffer(geometry, &gltf_asset, &buffer)); + + // Define a function for concatenating GLB file chunks into a single buffer. + const auto encode_chunk_to_buffer = + [&out_buffer](const EncoderBuffer &chunk) -> Status { + if (!out_buffer->Encode(chunk.data(), chunk.size())) { + return Status(Status::DRACO_ERROR, "Error writing to buffer."); + } + return OkStatus(); + }; + + // Create GLB file chunks and concatenate them to a single buffer. + return ProcessGlbFileChunks(gltf_asset, buffer, encode_chunk_to_buffer); +} + +// Explicit instantiation for Mesh and Scene. +template bool GltfEncoder::EncodeToFile(const Mesh &geometry, + const std::string &file_name, + const std::string &base_dir); +template bool GltfEncoder::EncodeToFile(const Scene &geometry, + const std::string &file_name, + const std::string &base_dir); +template Status GltfEncoder::EncodeFile(const Mesh &geometry, + const std::string &filename); +template Status GltfEncoder::EncodeFile(const Scene &geometry, + const std::string &filename); +template Status GltfEncoder::EncodeFile(const Mesh &geometry, + const std::string &filename, + const std::string &bin_filename); +template Status GltfEncoder::EncodeFile(const Scene &geometry, + const std::string &filename, + const std::string &bin_filename); +template Status GltfEncoder::EncodeFile(const Mesh &geometry, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); +template Status GltfEncoder::EncodeFile(const Scene &geometry, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); +template Status GltfEncoder::EncodeToBuffer(const Mesh &geometry, + EncoderBuffer *out_buffer); +template Status GltfEncoder::EncodeToBuffer(const Scene &geometry, + EncoderBuffer *out_buffer); + +Status GltfEncoder::EncodeToBuffer(const Mesh &mesh, GltfAsset *gltf_asset, + EncoderBuffer *out_buffer) { + out_buffer_ = out_buffer; + SetJsonWriterMode(gltf_asset); + if (!gltf_asset->AddDracoMesh(mesh)) { + return Status(Status::DRACO_ERROR, "Error adding Draco mesh."); + } + return gltf_asset->Output(out_buffer); +} + +Status GltfEncoder::EncodeToBuffer(const Scene &scene, GltfAsset *gltf_asset, + EncoderBuffer *out_buffer) { + out_buffer_ = out_buffer; + SetJsonWriterMode(gltf_asset); + DRACO_RETURN_IF_ERROR(gltf_asset->AddScene(scene)); + return gltf_asset->Output(out_buffer); +} + +void GltfEncoder::SetJsonWriterMode(class GltfAsset *gltf_asset) { + if (gltf_asset->output_type() == COMPACT && + gltf_asset->add_images_to_buffer()) { + gltf_asset->set_json_output_mode(JsonWriter::COMPACT); + } else { + gltf_asset->set_json_output_mode(JsonWriter::READABLE); + } +} + +Status GltfEncoder::WriteGltfFiles(const GltfAsset &gltf_asset, + const EncoderBuffer &buffer, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir) { + std::unique_ptr file = + FileWriterFactory::OpenWriter(filename); + if (!file) { + return Status(Status::DRACO_ERROR, "Output glTF file could not be opened."); + } + std::unique_ptr bin_file = + FileWriterFactory::OpenWriter(bin_filename); + if (!bin_file) { + return Status(Status::DRACO_ERROR, + "Output glTF bin file could not be opened."); + } + + // Write the glTF data into the file. + if (!file->Write(buffer.data(), buffer.size())) { + return Status(Status::DRACO_ERROR, "Error writing to glTF file."); + } + + // Write the glTF buffer into the file. + if (!bin_file->Write(gltf_asset.Buffer()->data(), + gltf_asset.Buffer()->size())) { + return Status(Status::DRACO_ERROR, "Error writing to glTF bin file."); + } + + for (int i = 0; i < gltf_asset.NumImages(); ++i) { + const std::string name = resource_dir + "/" + gltf_asset.image_name(i); + const GltfImage *const image = gltf_asset.GetImage(i); + if (!image) { + return Status(Status::DRACO_ERROR, "Error getting glTF image."); + } + DRACO_RETURN_IF_ERROR(WriteTextureToFile(name, *image->texture)); + } + return OkStatus(); +} + +Status GltfEncoder::WriteGlbFile(const GltfAsset &gltf_asset, + const EncoderBuffer &json_data, + const std::string &filename) { + std::unique_ptr file = + FileWriterFactory::OpenWriter(filename); + if (!file) { + return Status(Status::DRACO_ERROR, "Output glb file could not be opened."); + } + + // Define a function for writing GLB file chunks to |file|. + const auto write_chunk_to_file = + [&file](const EncoderBuffer &chunk) -> Status { + if (!file->Write(chunk.data(), chunk.size())) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + return OkStatus(); + }; + + // Create GLB file chunks and write them to file. + return ProcessGlbFileChunks(gltf_asset, json_data, write_chunk_to_file); +} + +Status GltfEncoder::ProcessGlbFileChunks( + const class GltfAsset &gltf_asset, const EncoderBuffer &json_data, + const std::function &process_chunk) const { + // The json data must be padded so the next chunk starts on a 4-byte boundary. + const uint32_t json_pad_length = + (json_data.size() % 4) ? 4 - json_data.size() % 4 : 0; + const uint32_t json_length = json_data.size() + json_pad_length; + const uint32_t total_length = + 12 + 8 + json_length + 8 + gltf_asset.Buffer()->size(); + + EncoderBuffer header; + // Write the glb file header. + const uint32_t gltf_version = 2; + if (!header.Encode("glTF", 4)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(gltf_version)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(total_length)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + + // Write the JSON chunk. + const uint32_t json_chunk_type = 0x4E4F534A; + if (!header.Encode(json_length)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(json_chunk_type)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + DRACO_RETURN_IF_ERROR(process_chunk(header)); + DRACO_RETURN_IF_ERROR(process_chunk(json_data)); + + // Pad the data if needed. + header.Clear(); + if (json_pad_length > 0) { + if (!header.Encode(" ", json_pad_length)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + } + + // Write the binary buffer chunk. + const uint32_t bin_chunk_type = 0x004E4942; + const uint32_t gltf_bin_size = gltf_asset.Buffer()->size(); + if (!header.Encode(gltf_bin_size)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + if (!header.Encode(bin_chunk_type)) { + return Status(Status::DRACO_ERROR, "Error writing to glb file."); + } + DRACO_RETURN_IF_ERROR(process_chunk(header)); + DRACO_RETURN_IF_ERROR(process_chunk(*gltf_asset.Buffer())); + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_encoder.h b/contrib/draco/src/draco/io/gltf_encoder.h new file mode 100644 index 000000000..16403ac31 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_encoder.h @@ -0,0 +1,134 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_GLTF_ENCODER_H_ +#define DRACO_IO_GLTF_ENCODER_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include + +#include "draco/core/encoder_buffer.h" +#include "draco/io/file_writer_factory.h" +#include "draco/io/file_writer_interface.h" +#include "draco/io/texture_io.h" +#include "draco/mesh/mesh.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Class for encoding draco::Mesh into the glTF file format. +class GltfEncoder { + public: + // Types of output modes for the glTF data encoder. |COMPACT| will output + // required and non-default glTF data. |VERBOSE| will output required and + // default glTF data as well as readable JSON even when the output is saved in + // a glTF-Binary file. + enum OutputType { COMPACT, VERBOSE }; + + GltfEncoder(); + + // Encodes the geometry and saves it into a file. Returns false when either + // the encoding failed or when the file couldn't be opened. + template + bool EncodeToFile(const T &geometry, const std::string &file_name, + const std::string &base_dir); + + // Saves |geometry| into glTF 2.0 format. |filename| is the name of the + // glTF file. The glTF bin file (if needed) will be named stem(|filename|) + + // “.bin”. The other files (if needed) will be saved to basedir(|filename|). + // If |filename| has the extension "glb" then |filename| will be written as a + // glTF-Binary file. Otherwise |filename| will be written as non-binary glTF + // file. + template + Status EncodeFile(const T &geometry, const std::string &filename); + + // Saves |geometry| into glTF 2.0 format. |filename| is the name of the + // glTF file. |bin_filename| is the name of the glTF bin file. The other + // files (if needed) will be saved to basedir(|filename|). |bin_filename| will + // be ignored if output is glTF-Binary. + template + Status EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename); + + // Saves |geometry| into glTF 2.0 format. |filename| is the name of the + // glTF file. |bin_filename| is the name of the glTF bin file. The other + // files will be saved to |resource_dir|. |bin_filename| and |resource_dir| + // will be ignored if output is glTF-Binary. + template + Status EncodeFile(const T &geometry, const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); + + // Encodes |geometry| to |out_buffer| in glTF 2.0 GLB format. + template + Status EncodeToBuffer(const T &geometry, EncoderBuffer *out_buffer); + + void set_output_type(OutputType type) { output_type_ = type; } + OutputType output_type() const { return output_type_; } + + // The name of the attribute metadata that contains the glTF attribute + // name. For application-specific generic attributes, if the metadata for + // an attribute contains this key, then the value will be used as the + // encoded attribute name in the output GLTF. + static const char kDracoMetadataGltfAttributeName[]; + + private: + // Encodes the mesh or the point cloud into a buffer. + Status EncodeToBuffer(const Mesh &mesh, class GltfAsset *gltf_asset, + EncoderBuffer *out_buffer); + Status EncodeToBuffer(const Scene &scene, class GltfAsset *gltf_asset, + EncoderBuffer *out_buffer); + + // Sets appropriate Json writer mode based on the provided |gltf_asset| + // options. + static void SetJsonWriterMode(class GltfAsset *gltf_asset); + + // Writes the ".gltf" and associted files. |gltf_asset| holds the glTF data. + // |buffer| is the encoded glTF json data. |filename| is the name of the + // ".gltf" file. |bin_filename| is the name of the glTF bin file. The other + // files will be saved to |resource_dir|. + Status WriteGltfFiles(const class GltfAsset &gltf_asset, + const EncoderBuffer &buffer, + const std::string &filename, + const std::string &bin_filename, + const std::string &resource_dir); + + // Writes the ".glb" file. |gltf_asset| holds the glTF data. |json_data| is + // the encoded glTF json data. |filename| is the name of the ".glb" file. + Status WriteGlbFile(const class GltfAsset &gltf_asset, + const EncoderBuffer &json_data, + const std::string &filename); + + // Creates GLB file chunks and passes them to |process_chunk| function for + // processing. |gltf_asset| holds the glTF data. |json_data| is the encoded + // glTF json data. + Status ProcessGlbFileChunks( + const class GltfAsset &gltf_asset, const EncoderBuffer &json_data, + const std::function &process_chunk) const; + + EncoderBuffer *out_buffer_; + OutputType output_type_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_ENCODER_H_ diff --git a/contrib/draco/src/draco/io/gltf_encoder_test.cc b/contrib/draco/src/draco/io/gltf_encoder_test.cc new file mode 100644 index 000000000..179256dfd --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_encoder_test.cc @@ -0,0 +1,1717 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_encoder.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_reader_factory.h" +#include "draco/io/file_reader_interface.h" +#include "draco/io/file_utils.h" +#include "draco/io/gltf_decoder.h" +#include "draco/io/gltf_test_helper.h" +#include "draco/io/parser_utils.h" +#include "draco/io/texture_io.h" +#include "draco/material/material_utils.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/mesh_group.h" +#include "draco/scene/scene.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +namespace { +std::unique_ptr DecodeFullPathGltfFileToScene( + const std::string &file_name) { + GltfDecoder decoder; + + auto maybe_scene = decoder.DecodeFromFileToScene(file_name); + if (!maybe_scene.ok()) { + std::cout << maybe_scene.status().error_msg_string() << std::endl; + return nullptr; + } + std::unique_ptr scene = std::move(maybe_scene).value(); + return scene; +} + +std::unique_ptr DecodeTestGltfFileToScene(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + return DecodeFullPathGltfFileToScene(path); +} +} // namespace + +class GltfEncoderTest : public ::testing::Test { + protected: + // This function searches for the |search| string and checks that there are at + // least |count| occurrences. + void CheckGltfFileAtLeastStringCount(const std::string &gltf_file, + const std::string &search, int count) { + std::vector data; + ASSERT_TRUE(ReadFileToBuffer(gltf_file, &data)); + + draco::DecoderBuffer buffer; + buffer.Init(data.data(), data.size()); + + int strings_found = 0; + do { + std::string gltf_line; + draco::parser::ParseLine(&buffer, &gltf_line); + if (gltf_line.empty()) { + break; + } + + if (gltf_line.find(search) != std::string::npos) { + strings_found++; + } + // No need to keep counting pass |count|. + } while (strings_found < count); + ASSERT_GE(strings_found, count); + } + + // This function searches for the |search| string and checks that there no + // occurrences. + void CheckGltfFileNoString(const std::string &gltf_file, + const std::string &search) { + std::vector data; + ASSERT_TRUE(ReadFileToBuffer(gltf_file, &data)); + + draco::DecoderBuffer buffer; + buffer.Init(data.data(), data.size()); + + do { + std::string gltf_line; + draco::parser::ParseLine(&buffer, &gltf_line); + if (gltf_line.empty()) { + break; + } + ASSERT_TRUE(gltf_line.find(search) == std::string::npos); + } while (true); + } + + void CheckAnimationAccessors(const Scene &scene, + int expected_num_input_accessors, + int expected_num_output_accessors) { + int num_input_accessors = 0; + int num_output_accessors = 0; + + for (int i = 0; i < scene.NumAnimations(); ++i) { + const Animation *anim = scene.GetAnimation(AnimationIndex(i)); + ASSERT_NE(anim, nullptr); + + // The animation accessors in Draco are relative to the Animation object. + // While in glTF the animation accessors are relative to the global + // accessors. + std::unordered_set seen_accessors; + + for (int j = 0; j < anim->NumSamplers(); ++j) { + const AnimationSampler *const sampler = anim->GetSampler(j); + ASSERT_NE(sampler, nullptr); + + if (seen_accessors.find(sampler->input_index) == seen_accessors.end()) { + seen_accessors.insert(sampler->input_index); + num_input_accessors++; + } + if (seen_accessors.find(sampler->output_index) == + seen_accessors.end()) { + seen_accessors.insert(sampler->output_index); + num_output_accessors++; + } + } + } + + EXPECT_EQ(expected_num_input_accessors, num_input_accessors); + EXPECT_EQ(expected_num_output_accessors, num_output_accessors); + } + + void CompareMeshes(const Mesh *mesh0, const Mesh *mesh1) { + ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces()); + ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes()); + for (int att_id = 0; att_id < mesh0->num_attributes(); ++att_id) { + const GeometryAttribute::Type att_type = + mesh0->attribute(att_id)->attribute_type(); + const PointAttribute *const att = mesh1->GetNamedAttribute(att_type); + ASSERT_EQ(mesh0->attribute(att_id)->size(), att->size()) + << "Attribute id:" << att_id << " is not equal."; + } + + // Check materials are the same. + if (mesh0->GetMaterialLibrary().NumMaterials() == 0) { + // We add a default material if the source had no materials. + ASSERT_EQ(mesh1->GetMaterialLibrary().NumMaterials(), 1); + } else if (mesh1->GetMaterialLibrary().NumMaterials() == 0) { + // We add a default material if the source had no materials. + ASSERT_EQ(mesh0->GetMaterialLibrary().NumMaterials(), 1); + } else { + ASSERT_EQ(mesh0->GetMaterialLibrary().NumMaterials(), + mesh1->GetMaterialLibrary().NumMaterials()); + for (int i = 0; i < mesh0->GetMaterialLibrary().NumMaterials(); ++i) { + ASSERT_EQ(mesh0->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps(), + mesh1->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps()); + ASSERT_EQ(mesh0->GetMaterialLibrary().GetMaterial(i)->GetName(), + mesh1->GetMaterialLibrary().GetMaterial(i)->GetName()); + } + } + } + + void CompareScenes(const Scene *scene0, const Scene *scene1) { + ASSERT_EQ(scene0->NumMeshes(), scene1->NumMeshes()); + ASSERT_EQ(scene0->NumMeshGroups(), scene1->NumMeshGroups()); + ASSERT_EQ(scene0->NumNodes(), scene1->NumNodes()); + ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterials(), + scene1->GetMaterialLibrary().NumMaterials()); + ASSERT_EQ(scene0->NumAnimations(), scene1->NumAnimations()); + ASSERT_EQ(scene0->NumSkins(), scene1->NumSkins()); + ASSERT_EQ(scene0->NumLights(), scene1->NumLights()); + + // Check materials are the same. + for (int i = 0; i < scene0->GetMaterialLibrary().NumMaterials(); ++i) { + ASSERT_EQ(scene0->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps(), + scene1->GetMaterialLibrary().GetMaterial(i)->NumTextureMaps()); + ASSERT_EQ(scene0->GetMaterialLibrary().GetMaterial(i)->GetName(), + scene1->GetMaterialLibrary().GetMaterial(i)->GetName()); + } + + // Check that materials variants names are the same. + ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterialsVariants(), + scene1->GetMaterialLibrary().NumMaterialsVariants()); + for (int i = 0; i < scene0->GetMaterialLibrary().NumMaterialsVariants(); + i++) { + ASSERT_EQ(scene0->GetMaterialLibrary().GetMaterialsVariantName(i), + scene1->GetMaterialLibrary().GetMaterialsVariantName(i)); + } + + // Check Nodes are the same. + for (draco::SceneNodeIndex i(0); i < scene0->NumNodes(); ++i) { + const SceneNode *const scene_node0 = scene0->GetNode(i); + const SceneNode *const scene_node1 = scene1->GetNode(i); + ASSERT_NE(scene_node0, nullptr); + ASSERT_NE(scene_node1, nullptr); + ASSERT_EQ(scene_node0->GetName(), scene_node1->GetName()); + ASSERT_EQ(scene_node0->GetLightIndex(), scene_node1->GetLightIndex()); + } + + // Check MeshGroups are the same. + for (draco::MeshGroupIndex i(0); i < scene0->NumMeshGroups(); ++i) { + const MeshGroup *const mesh_group0 = scene0->GetMeshGroup(i); + const MeshGroup *const mesh_group1 = scene1->GetMeshGroup(i); + ASSERT_NE(mesh_group0, nullptr); + ASSERT_NE(mesh_group1, nullptr); + ASSERT_EQ(mesh_group0->GetName(), mesh_group1->GetName()); + ASSERT_EQ(mesh_group0->NumMeshInstances(), + mesh_group1->NumMeshInstances()); + + // Check that mesh instanes are the same. + for (int j = 0; j < mesh_group1->NumMeshInstances(); j++) { + const MeshGroup::MeshInstance &instance0 = + mesh_group0->GetMeshInstance(j); + const MeshGroup::MeshInstance &instance1 = + mesh_group1->GetMeshInstance(j); + ASSERT_EQ(instance0.mesh_index, instance1.mesh_index); + ASSERT_EQ(instance0.material_index, instance1.material_index); + ASSERT_EQ(instance0.materials_variants_mappings.size(), + instance1.materials_variants_mappings.size()); + + // Check that materials variants mappings are the same. + for (int k = 0; k < instance0.materials_variants_mappings.size(); k++) { + const MeshGroup::MaterialsVariantsMapping &mapping0 = + instance0.materials_variants_mappings[k]; + const MeshGroup::MaterialsVariantsMapping &mapping1 = + instance1.materials_variants_mappings[k]; + ASSERT_EQ(mapping0.material, mapping1.material); + ASSERT_EQ(mapping0.variants.size(), mapping1.variants.size()); + for (int l = 0; l < mapping0.variants.size(); l++) { + ASSERT_EQ(mapping0.variants[l], mapping1.variants[l]); + } + } + } + } + + // Check Animations are the same. + for (draco::AnimationIndex i(0); i < scene0->NumAnimations(); ++i) { + const Animation *const animation0 = scene0->GetAnimation(i); + const Animation *const animation1 = scene1->GetAnimation(i); + ASSERT_NE(animation0, nullptr); + ASSERT_NE(animation1, nullptr); + ASSERT_EQ(animation0->NumSamplers(), animation1->NumSamplers()); + ASSERT_EQ(animation0->NumChannels(), animation1->NumChannels()); + ASSERT_EQ(animation0->NumNodeAnimationData(), + animation1->NumNodeAnimationData()); + } + + // Check that lights are the same. + for (draco::LightIndex i(0); i < scene0->NumLights(); ++i) { + const Light *const light0 = scene0->GetLight(i); + const Light *const light1 = scene1->GetLight(i); + ASSERT_NE(light0, nullptr); + ASSERT_NE(light1, nullptr); + ASSERT_EQ(light0->GetName(), light1->GetName()); + ASSERT_EQ(light0->GetColor(), light1->GetColor()); + ASSERT_EQ(light0->GetIntensity(), light1->GetIntensity()); + ASSERT_EQ(light0->GetType(), light1->GetType()); + ASSERT_EQ(light0->GetRange(), light1->GetRange()); + if (light0->GetType() == Light::SPOT) { + ASSERT_EQ(light0->GetInnerConeAngle(), light1->GetInnerConeAngle()); + ASSERT_EQ(light0->GetOuterConeAngle(), light1->GetOuterConeAngle()); + } + } + } + + void EncodeMeshToFile(const Mesh &mesh, + const std::string &gltf_file_full_path) { + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + ASSERT_TRUE( + gltf_encoder.EncodeToFile(mesh, gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + } + + void EncodeSceneToFile(const Scene &scene, + const std::string &gltf_file_full_path) { + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeToFile(scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + } + + // Encode |mesh| to a temporary glTF file. Then decode the glTF file and + // return the mesh in |mesh_gltf|. + void MeshToDecodedGltfMesh(const Mesh &mesh, + std::unique_ptr *mesh_gltf) { + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + EncodeMeshToFile(mesh, gltf_file_full_path); + *mesh_gltf = std::move(ReadMeshFromFile(gltf_file_full_path)).value(); + ASSERT_NE(*mesh_gltf, nullptr); + } + + // Encode |mesh| to a temporary glTF file. Then decode the glTF file as a + // scene and return it in |scene_gltf|. + void MeshToDecodedGltfScene(const Mesh &mesh, + std::unique_ptr *scene_gltf) { + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + EncodeMeshToFile(mesh, gltf_file_full_path); + *scene_gltf = std::move(ReadSceneFromFile(gltf_file_full_path)).value(); + ASSERT_NE(*scene_gltf, nullptr); + } + + // Encode |scene| to a temporary glTF file. Then decode the glTF file and + // return the scene in |scene_gltf|. + void SceneToDecodedGltfScene(const Scene &scene, + const std::string &temp_basename, + std::unique_ptr *scene_gltf) { + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath(temp_basename); + EncodeSceneToFile(scene, gltf_file_full_path); + + *scene_gltf = DecodeFullPathGltfFileToScene(gltf_file_full_path); + if (SceneUtils::IsDracoCompressionEnabled(scene)) { + // Two occurrences of the Draco compression string is the least amount for + // a valid Draco compressed glTF file. + const std::string khr_draco_compression = "KHR_draco_mesh_compression"; + CheckGltfFileAtLeastStringCount(gltf_file_full_path, + khr_draco_compression, 2); + } + ASSERT_NE((*scene_gltf).get(), nullptr); + } + + void SceneToDecodedGltfScene(const Scene &scene, + std::unique_ptr *scene_gltf) { + SceneToDecodedGltfScene(scene, "test.gltf", scene_gltf); + } + + void EncodeMeshToGltfAndCompare(Mesh *mesh) { + ASSERT_GT(mesh->num_faces(), 0); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + + mesh->DeduplicatePointIds(); + ASSERT_TRUE(mesh->DeduplicateAttributeValues()); + CompareMeshes(mesh, mesh_from_gltf.get()); + } + + void EncodeSceneToGltfAndCompare(Scene *scene) { + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + if (!SceneUtils::IsDracoCompressionEnabled(*scene)) { + CompareScenes(scene, scene_from_gltf.get()); + } + } + + void test_encoding(const std::string &file_name) { + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name, true)); + + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + EncodeMeshToGltfAndCompare(mesh.get()); + } +}; + +TEST_F(GltfEncoderTest, TestGltfEncodingAll) { + // Test decoded mesh from encoded glTF file stays the same. + test_encoding("test_nm.obj.edgebreaker.cl4.2.2.drc"); + test_encoding("cube_att.drc"); + test_encoding("car.drc"); + test_encoding("bunny_gltf.drc"); +} + +TEST_F(GltfEncoderTest, ImportTangentAttribute) { + auto mesh = draco::ReadMeshFromTestFile("sphere.gltf"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const tangent_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::TANGENT); + ASSERT_NE(tangent_att, nullptr); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_EQ(mesh->num_attributes(), mesh_from_gltf->num_attributes()); +} + +TEST_F(GltfEncoderTest, EncodeColorTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::COLOR, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +TEST_F(GltfEncoderTest, EncodeColors) { + auto mesh = draco::ReadMeshFromTestFile("test_pos_color.ply"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const color_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::COLOR); + ASSERT_NE(color_att, nullptr); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + + ASSERT_EQ(mesh->num_faces(), mesh_from_gltf->num_faces()); + ASSERT_EQ(mesh->num_attributes(), mesh_from_gltf->num_attributes()); + ASSERT_EQ( + mesh->NumNamedAttributes(draco::GeometryAttribute::COLOR), + mesh_from_gltf->NumNamedAttributes(draco::GeometryAttribute::COLOR)); +} + +TEST_F(GltfEncoderTest, EncodeNamedGenericAttribute) { + // Load some base mesh. + auto mesh = draco::ReadMeshFromTestFile("test_generic.ply"); + ASSERT_NE(mesh, nullptr); + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att, nullptr); + int num_vertices = pos_att->size(); + + // Add two new scalar attributes where each value corresponds to the position + // value index (vertex). The first attribute will have metadata, the second + // attribute won't. + std::unique_ptr pa_0(new draco::PointAttribute()); + std::unique_ptr pa_1(new draco::PointAttribute()); + pa_0->Init(draco::GeometryAttribute::GENERIC, /* scalar */ 1, + draco::DT_FLOAT32, false, + /* one value per position value */ num_vertices); + pa_1->Init(draco::GeometryAttribute::GENERIC, /* scalar */ 1, + draco::DT_FLOAT32, false, + /* one value per position value */ num_vertices); + + // Set the values for the new attributes. + for (draco::AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const float att_value = avi.value(); + pa_0->SetAttributeValue(avi, &att_value); + pa_1->SetAttributeValue(avi, &att_value); + } + + // Add the attribute to the existing mesh. + const int new_att_id_0 = mesh->AddPerVertexAttribute(std::move(pa_0)); + const int new_att_id_1 = mesh->AddPerVertexAttribute(std::move(pa_1)); + ASSERT_NE(new_att_id_0, -1); + ASSERT_NE(new_att_id_1, -1); + + // Set metadata for first attribute so it gets written out by glTF encoder. + std::unique_ptr am(new draco::AttributeMetadata()); + constexpr char kAttributeName[] = "MyAttributeName"; + constexpr char kDracoMetadataGltfAttributeName[] = + "//GLTF/ApplicationSpecificAttributeName"; + am->AddEntryString(kDracoMetadataGltfAttributeName, kAttributeName); + mesh->AddAttributeMetadata(new_att_id_0, std::move(am)); + + // Make sure the GLTF contains a reference to the named attribute. + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("GenericAttribute.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeToFile(*(mesh.get()), + gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + CheckGltfFileAtLeastStringCount(gltf_file_full_path, kAttributeName, 1); + + // The decoder does not yet support generic attribute names, so instead of + // using the decoder we compare against a golden file. + const std::string gltf_generated_bin_filename = + draco::GetTestTempFileFullPath("buffer0.bin"); + std::vector generated_buffer; + ASSERT_TRUE(ReadFileToBuffer(gltf_generated_bin_filename, &generated_buffer)); + std::string generated_str(generated_buffer.data(), generated_buffer.size()); + + const std::string gltf_expected_bin_filename = + GetTestFileFullPath("test_generic_golden.bin"); + const bool kUpdateGoldens = false; + if (kUpdateGoldens) { + ASSERT_TRUE(WriteBufferToFile(generated_buffer.data(), + generated_buffer.size(), + gltf_expected_bin_filename)); + } + std::vector expected_buffer; + ASSERT_TRUE(ReadFileToBuffer(gltf_expected_bin_filename, &expected_buffer)); + std::string expected_str(expected_buffer.data(), expected_buffer.size()); + + EXPECT_TRUE(generated_str == expected_str); +} + +TEST_F(GltfEncoderTest, EncodeMetallicRoughnessTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::METALLIC_ROUGHNESS, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +TEST_F(GltfEncoderTest, EncodeOcclusionTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::AMBIENT_OCCLUSION, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +TEST_F(GltfEncoderTest, EncodeEmissiveTexture) { + const std::string tex_file_name = draco::GetTestFileFullPath("test.png"); + std::unique_ptr texture = + draco::ReadTextureFromFile(tex_file_name).value(); + ASSERT_NE(texture, nullptr); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + mesh->GetMaterialLibrary().MutableMaterial(0)->SetTextureMap( + std::move(texture), draco::TextureMap::EMISSIVE, 0); + + EncodeMeshToGltfAndCompare(mesh.get()); +} + +// Tests splitting the mesh into multiple primitives. +TEST_F(GltfEncoderTest, EncodeSplitMesh) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(mesh, nullptr); + const int32_t material_att_id = + mesh->GetNamedAttributeId(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(material_att_id, -1); + EncodeMeshToGltfAndCompare(mesh.get()); +} + +// Tests encoding a scene from a glTF with multiple meshes and primitives, +// including mesh instances. +TEST_F(GltfEncoderTest, EncodeInstancedScene) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr transcoded_scene; + SceneToDecodedGltfScene(*scene, "EncodeInstancedScene.gltf", + &transcoded_scene); + ASSERT_NE(transcoded_scene, nullptr); + CompareScenes(scene.get(), transcoded_scene.get()); + EXPECT_EQ(transcoded_scene->NumAnimations(), 1); + + const int num_input_accessors = 2; + const int num_output_accessors = 2; + CheckAnimationAccessors(*transcoded_scene, num_input_accessors, + num_output_accessors); +} + +// Tests encoding a scene from a glTF with multiple meshes and primitives, +// including mesh instances. +TEST_F(GltfEncoderTest, EncodeBoneAnimation) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr transcoded_scene; + SceneToDecodedGltfScene(*scene, "EncodeBoneAnimation.gltf", + &transcoded_scene); + ASSERT_NE(transcoded_scene, nullptr); + CompareScenes(scene.get(), transcoded_scene.get()); + EXPECT_EQ(transcoded_scene->NumAnimations(), 1); + + const Animation *anim = scene->GetAnimation(AnimationIndex(0)); + ASSERT_NE(anim, nullptr); + ASSERT_TRUE(anim->GetName().empty()); + + // TODO(b/145703399): Figure out how to test that all of the input accessors + // in animation channels in the encoded glTF file will be the same for this + // test file. + const int num_input_accessors = 57; + const int num_output_accessors = 57; + CheckAnimationAccessors(*transcoded_scene, num_input_accessors, + num_output_accessors); +} + +// Tests encoding a scene from a glTF with nodes that have names. +TEST_F(GltfEncoderTest, EncodeSceneWithNodeNames) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + EncodeSceneToGltfAndCompare(scene.get()); +} + +// Tests encoding a simple glTF with Draco compression. +TEST_F(GltfEncoderTest, EncodeWithDracoCompression) { + const std::string file_name = "Box/glTF/Box.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, EncodeWeightsJointsWithDracoCompression) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, EncodeTangentsWithDracoCompression) { + const std::string file_name = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, TestDracoCompressionWithGeneratedPoints) { + const std::string basename = "test_nm.obj"; + std::unique_ptr mesh = draco::ReadMeshFromTestFile(basename); + ASSERT_NE(mesh, nullptr) << "Failed to load " << basename; + + auto maybe_scene = draco::SceneUtils::MeshToScene(std::move(mesh)); + ASSERT_TRUE(maybe_scene.ok()) << "Failed Mesh to Scene conversion."; + const std::unique_ptr scene = std::move(maybe_scene).value(); + ASSERT_NE(scene, nullptr); + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + EncodeSceneToGltfAndCompare(scene.get()); +} + +TEST_F(GltfEncoderTest, TestDracoCompressionWithDegenerateFaces) { + const std::string basename = "deg_faces.obj"; + std::unique_ptr mesh = draco::ReadMeshFromTestFile(basename); + ASSERT_NE(mesh, nullptr) << "Failed to load " << basename; + ASSERT_EQ(mesh->num_faces(), 4); + + auto maybe_scene = draco::SceneUtils::MeshToScene(std::move(mesh)); + ASSERT_TRUE(maybe_scene.ok()) << "Failed Mesh to Scene conversion."; + const std::unique_ptr scene = std::move(maybe_scene).value(); + ASSERT_NE(scene, nullptr); + const Mesh &scene_first_mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_EQ(scene_first_mesh.num_faces(), 4); + + std::unique_ptr scene_from_gltf; + const DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + const Mesh &scene_from_gltf_first_mesh = + scene_from_gltf->GetMesh(MeshIndex(0)); + ASSERT_EQ(scene_from_gltf_first_mesh.num_faces(), 3); + + CompareScenes(scene.get(), scene_from_gltf.get()); +} + +TEST_F(GltfEncoderTest, DracoCompressionCheckOptions) { + const std::string file_name = "CesiumMan/glTF/CesiumMan.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + + const std::string gltf_bin_filename = + draco::GetTestTempFileFullPath("buffer0.bin"); + const size_t default_bin_size = draco::GetFileSize(gltf_bin_filename); + + // Test applying more quantization will make the compressed size smaller. + options.quantization_position.SetQuantizationBits(6); + options.quantization_bits_normal = 6; + options.quantization_bits_tex_coord = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t more_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(more_quantization_bin_size, default_bin_size); + + // Test setting more weight quantization then the default makes the compressed + // size smaller. + options.quantization_bits_weight = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t more_weight_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(more_weight_quantization_bin_size, more_quantization_bin_size); + + options.quantization_position.SetQuantizationBits(20); + options.quantization_bits_normal = 20; + options.quantization_bits_tex_coord = 20; + options.quantization_bits_weight = 20; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t less_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_GT(less_quantization_bin_size, default_bin_size); + + DracoCompressionOptions level_options; + level_options.compression_level = 10; // compression level [0-10]. + SceneUtils::SetDracoCompressionOptions(&level_options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t most_compression_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(most_compression_bin_size, default_bin_size); + + level_options.compression_level = 4; + SceneUtils::SetDracoCompressionOptions(&level_options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t less_compression_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_GT(less_compression_bin_size, default_bin_size); + + level_options.compression_level = 0; + SceneUtils::SetDracoCompressionOptions(&level_options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t least_compression_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_GT(least_compression_bin_size, less_compression_bin_size); +} + +TEST_F(GltfEncoderTest, TestQuantizationPerAttribute) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + DracoCompressionOptions options; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + + const std::string gltf_bin_filename = + draco::GetTestTempFileFullPath("buffer0.bin"); + const size_t default_bin_size = draco::GetFileSize(gltf_bin_filename); + + // Test setting more position quantization then the default makes the + // compressed size smaller. + options.quantization_position.SetQuantizationBits(6); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t position_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(position_quantization_bin_size, default_bin_size); + + // Test setting more normal quantization then the default makes the compressed + // size smaller. + options.quantization_bits_normal = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t normal_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(normal_quantization_bin_size, position_quantization_bin_size); + + // Test setting more tex_coord quantization then the default makes the + // compressed size smaller. + options.quantization_bits_tex_coord = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t tex_coord_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(tex_coord_quantization_bin_size, normal_quantization_bin_size); + + // Test setting more tangent quantization then the default makes the + // compressed size smaller. Weight is tested in DracoCompressionCheckOptions. + options.quantization_bits_tangent = 6; + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t tangent_quantization_bin_size = + draco::GetFileSize(gltf_bin_filename); + ASSERT_LT(tangent_quantization_bin_size, tex_coord_quantization_bin_size); +} + +// Tests encoding a glTF with multiple scaled instances with Draco compression +// using grid options for position quantization. +TEST_F(GltfEncoderTest, TestDracoCompressionWithGridOptions) { + const std::string file_name = + "SpheresScaledInstances/glTF/spheres_scaled_instances.gltf"; + std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const auto bbox = scene->GetMesh(MeshIndex(0)).ComputeBoundingBox(); + const float mesh_size = bbox.Size().MaxCoeff(); + + // All dimensions of the original mesh are between [-1, 1]. Let's move the + // mesh to [0, 2] which will allow us to match grid quantization with the + // regular quantization (grid quantization is always aligned with 0). + Mesh &mesh = scene->GetMesh(MeshIndex(0)); + PointAttribute *pos_att = + mesh.attribute(mesh.GetNamedAttributeId(GeometryAttribute::POSITION)); + for (AttributeValueIndex avi(0); avi < pos_att->size(); ++avi) { + Vector3f pos; + pos_att->GetValue(avi, &pos[0]); + pos += Vector3f(1.f, 1.f, 1.f); + pos_att->SetAttributeValue(avi, &pos[0]); + } + + DracoCompressionOptions options; + + // First quantize the scene with 8 bits and save the result. + options.quantization_position.SetQuantizationBits(8); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + const std::string gltf_filename = draco::GetTestTempFileFullPath("temp.glb"); + GltfEncoder encoder; + DRACO_ASSERT_OK(encoder.EncodeFile(*scene, gltf_filename)); + // Get the size of the generated file. + const size_t qb_file_size = draco::GetFileSize(gltf_filename); + + // Now set grid quantization and ensure the encoded file size is about the + // same. The max instance scale is 3 and model size is |mesh_size| so the grid + // scale must account for that. + options.quantization_position.SetGrid(mesh_size * 3. / 255.); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + DRACO_ASSERT_OK(encoder.EncodeFile(*scene, gltf_filename)); + // Get the size of the generated file. + const size_t grid_file_size = draco::GetFileSize(gltf_filename); + + ASSERT_EQ(grid_file_size, qb_file_size); + + // Now set grid quantization to different settings and ensure the encoded size + // changed. We reduce spacing which should increase the size. + options.quantization_position.SetGrid(mesh_size * 3. / 511.); + SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + + DRACO_ASSERT_OK(encoder.EncodeFile(*scene, gltf_filename)); + + // Get the size of the generated file. + const size_t grid_file_size_2 = draco::GetFileSize(gltf_filename); + ASSERT_GT(grid_file_size_2, grid_file_size); +} + +TEST_F(GltfEncoderTest, TestOutputType) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("test.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + + const size_t default_gltf_size = draco::GetFileSize(gltf_file_full_path); + + // Test setting VERBOSE output type will increase the size of the gltf file. + gltf_encoder.set_output_type(GltfEncoder::VERBOSE); + ASSERT_TRUE(gltf_encoder.EncodeToFile(*scene, gltf_file_full_path, + folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + const size_t verbose_gltf_size = draco::GetFileSize(gltf_file_full_path); + ASSERT_GT(verbose_gltf_size, default_gltf_size); +} + +// Tests copying the name of the input texture file to the encoded texture file. +TEST_F(GltfEncoderTest, CopyTextureName) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(mesh, nullptr); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + const Material *material = mesh->GetMaterialLibrary().GetMaterial(0); + ASSERT_NE(material, nullptr); + const Texture *texture = + mesh->GetMaterialLibrary().GetTextureLibrary().GetTexture(0); + ASSERT_NE(texture, nullptr); + ASSERT_EQ(draco::TextureUtils::GetTargetStem(*texture), "CesiumMilkTruck"); + ASSERT_EQ(draco::TextureUtils::GetTargetFormat(*texture), + draco::ImageFormat::PNG); +} + +TEST_F(GltfEncoderTest, EncodeTexCoord1) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("MultiUVTest/glTF/MultiUVTest.gltf"); + + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_EQ(mesh_from_gltf->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ( + mesh_from_gltf->GetMaterialLibrary().GetMaterial(0)->NumTextureMaps(), 2); + ASSERT_EQ( + mesh_from_gltf->GetMaterialLibrary().GetTextureLibrary().NumTextures(), + 2); + const std::vector textures = { + mesh_from_gltf->GetMaterialLibrary().GetTextureLibrary().GetTexture(0), + mesh_from_gltf->GetMaterialLibrary().GetTextureLibrary().GetTexture(1)}; + EXPECT_EQ(draco::TextureUtils::GetTargetStem(*textures[0]), "uv0"); + EXPECT_EQ(draco::TextureUtils::GetTargetStem(*textures[1]), "uv1"); + EXPECT_EQ(draco::TextureUtils::GetTargetFormat(*textures[0]), + draco::ImageFormat::PNG); + EXPECT_EQ(draco::TextureUtils::GetTargetFormat(*textures[1]), + draco::ImageFormat::PNG); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::TEX_COORD), + 2); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::POSITION), 1); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::NORMAL), 1); + ASSERT_EQ(mesh_from_gltf->NumNamedAttributes(GeometryAttribute::TANGENT), 1); +} + +TEST_F(GltfEncoderTest, TestEncodeFileFunctions) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Test encoding with only the gltf filename parameter will output the correct + // bin filename and the textures will be in the same directory as the output + // glTF file. + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("encoded_example.bin"); + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); + const std::string output_png_filename = + draco::GetTestTempFileFullPath("sphere_Texture0_Normal.png"); + const size_t output_png_size = draco::GetFileSize(output_png_filename); + ASSERT_GT(output_png_size, 0); + + // Test encoding with the gltf and bin filename parameter, the textures will + // be in the same directory as the output glTF file. + const std::string new_bin_filename = + draco::GetTestTempFileFullPath("different_stem_name.bin"); + ASSERT_TRUE( + gltf_encoder + .EncodeFile(*scene, output_gltf_filename, new_bin_filename) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + const size_t new_bin_size = draco::GetFileSize(new_bin_filename); + ASSERT_GT(new_bin_size, 0); + ASSERT_EQ(new_bin_size, output_bin_size); + + // Test encoding with the gltf and bin filename and resource_dir parameter, + // the textures will be in the resource_dir directory. + const std::string new_resource_dir = output_gltf_dir + "/textures"; + ASSERT_TRUE(gltf_encoder + .EncodeFile(*scene, output_gltf_filename, + new_bin_filename, new_resource_dir) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + const std::string new_png_filename = + draco::GetTestTempFileFullPath("textures/sphere_Texture0_Normal.png"); + const size_t newest_bin_size = draco::GetFileSize(new_bin_filename); + ASSERT_GT(new_bin_size, 0); + ASSERT_EQ(new_bin_size, output_bin_size); + ASSERT_EQ(newest_bin_size, new_bin_size); + const size_t new_png_size = draco::GetFileSize(new_png_filename); + ASSERT_GT(new_png_size, 0); + ASSERT_EQ(new_png_size, output_png_size); +} + +TEST_F(GltfEncoderTest, DoubleSidedMaterial) { + const std::string file_name = "TwoSidedPlane/glTF/TwoSidedPlane.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + EXPECT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ(scene->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), true); + + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + EXPECT_EQ(scene_from_gltf->GetMaterialLibrary().NumMaterials(), 1); + EXPECT_EQ( + scene_from_gltf->GetMaterialLibrary().GetMaterial(0)->GetDoubleSided(), + true); +} + +TEST_F(GltfEncoderTest, EncodeGlb) { + const std::string file_name = "sphere.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, "temp.gltf", &scene_from_gltf); + + std::unique_ptr scene_from_glb; + SceneToDecodedGltfScene(*scene, "temp.glb", &scene_from_glb); + + CompareScenes(scene_from_gltf.get(), scene_from_glb.get()); +} + +TEST_F(GltfEncoderTest, EncodeVertexColor) { + const std::string file_name = "VertexColorTest/glTF/VertexColorTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + EXPECT_EQ(scene->NumMeshes(), 2); + const Mesh &mesh = scene->GetMesh(MeshIndex(1)); + EXPECT_EQ(mesh.NumNamedAttributes(GeometryAttribute::COLOR), 1); + + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, "temp.gltf", &scene_from_gltf); + EXPECT_EQ(scene_from_gltf->NumMeshes(), 2); + const Mesh &encoded_mesh = scene_from_gltf->GetMesh(MeshIndex(1)); + EXPECT_EQ(encoded_mesh.NumNamedAttributes(GeometryAttribute::COLOR), 1); +} + +TEST_F(GltfEncoderTest, InterpolationTest) { + const std::string file_name = "InterpolationTest/glTF/InterpolationTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + std::unique_ptr transcoded_scene; + SceneToDecodedGltfScene(*scene, "InterpolationTest.gltf", &transcoded_scene); + ASSERT_NE(transcoded_scene, nullptr); + CompareScenes(scene.get(), transcoded_scene.get()); + EXPECT_EQ(transcoded_scene->NumAnimations(), 9); + + const std::vector animation_names{ + "Step Scale", "Linear Scale", + "CubicSpline Scale", "Step Rotation", + "CubicSpline Rotation", "Linear Rotation", + "Step Translation", "CubicSpline Translation", + "Linear Translation"}; + for (int i = 0; i < scene->NumAnimations(); ++i) { + const Animation *const anim = scene->GetAnimation(AnimationIndex(i)); + ASSERT_NE(anim, nullptr); + ASSERT_EQ(anim->GetName(), animation_names[i]); + } + + // Currently all animation data is unique. See b/145703399. + const int num_input_accessors = 9; + const int num_output_accessors = 9; + CheckAnimationAccessors(*transcoded_scene, num_input_accessors, + num_output_accessors); +} + +TEST_F(GltfEncoderTest, KhrMaterialUnlit) { + const std::string filename = + "KhronosSampleModels/UnlitTest/glTF/UnlitTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have four occurences of "KHR_materials_unlit". Two in the + // materials and one in extensionsUsed and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 4); +} + +TEST_F(GltfEncoderTest, OneMaterialUnlitWithFallback) { + const std::string filename = + "UnlitWithFallback/one_material_all_fallback/" + "one_material_all_fallback.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + // glTF file should have two occurences of "KHR_materials_unlit". One in the + // materials and one in extensionsUsed. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 2); + + // The glTF file should provide a fallback to "KHR_materials_unlit", so there + // should be no "extensionsRequired" element. + CheckGltfFileNoString(output_gltf_filename, "extensionsRequired"); +} + +TEST_F(GltfEncoderTest, MultipleMaterialsUnlitWithFallback) { + std::string filename = + "UnlitWithFallback/three_materials_all_fallback/" + "three_materials_all_fallback.gltf"; + const std::unique_ptr scene_all_fallback( + DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene_all_fallback, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE( + gltf_encoder.EncodeFile(*scene_all_fallback, output_gltf_filename) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + // glTF file should have four occurences of "KHR_materials_unlit". Three in + // the materials and one in extensionsUsed. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 4); + + // The glTF file should provide a fallback to "KHR_materials_unlit", so there + // should be no "extensionsRequired" element. + CheckGltfFileNoString(output_gltf_filename, "extensionsRequired"); + + filename = + "UnlitWithFallback/three_materials_one_fallback/" + "three_materials_one_fallback.gltf"; + const std::unique_ptr scene_one_fallback( + DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene_one_fallback, nullptr); + + ASSERT_TRUE( + gltf_encoder.EncodeFile(*scene_one_fallback, output_gltf_filename) + .ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + + // glTF file should have three occurences of "KHR_materials_unlit". One in the + // materials, one in extensionsUsed, and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_materials_unlit", + 3); + + // The glTF file only has one material with a fallback for + // "KHR_materials_unlit". The other two materials have "KHR_materials_unlit" + // set without a fallback, so there should be an "extensionsRequired" element. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "extensionsRequired", + 1); +} + +TEST_F(GltfEncoderTest, KhrMaterialsSheenExtension) { + const std::string filename = + "KhronosSampleModels/SheenCloth/glTF/SheenCloth.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string out_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(out_filename, &output_gltf_dir, &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, out_filename).ok()) + << "Failed to encode glTF filename:" << out_filename; + + // The "KHR_materials_sheen" should be in material and in extensionsUsed. + CheckGltfFileAtLeastStringCount(out_filename, "KHR_materials_sheen", 2); + CheckGltfFileAtLeastStringCount(out_filename, "sheenColorFactor", 1); + CheckGltfFileAtLeastStringCount(out_filename, "sheenColorTexture", 1); + CheckGltfFileAtLeastStringCount(out_filename, "sheenRoughnessFactor", 1); + CheckGltfFileAtLeastStringCount(out_filename, "sheenRoughnessTexture", 1); +} + +TEST_F(GltfEncoderTest, PbrNextExtensions) { + // Check that a model with PBR material extensions is encoded correctly. This + // is done by encoding an original model with all PBR material extension + // properties and textures, then decoding it and checking that it matches the + // original model. + // TODO(vytyaz): Test multiple materials with various sets of extensions. + + // Read the original model. + const std::string orig_name = "pbr_next/sphere/glTF/sphere.gltf"; + const std::unique_ptr original(DecodeTestGltfFileToScene(orig_name)); + ASSERT_NE(original, nullptr); + const Material &original_mat = *original->GetMaterialLibrary().GetMaterial(0); + + // Check that the original material has PBR extensions. + EXPECT_TRUE(original_mat.HasSheen()); + EXPECT_TRUE(original_mat.HasTransmission()); + EXPECT_TRUE(original_mat.HasClearcoat()); + EXPECT_TRUE(original_mat.HasVolume()); + EXPECT_TRUE(original_mat.HasIor()); + EXPECT_TRUE(original_mat.HasSpecular()); + + // Write the original model to a temporary file. + GltfEncoder encoder; + const std::string tmp_name = draco::GetTestTempFileFullPath("tmp.gltf"); + DRACO_ASSERT_OK(encoder.EncodeFile(*original, tmp_name)); + + // Read model from the temporay file. + GltfDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(auto encoded, decoder.DecodeFromFileToScene(tmp_name)); + ASSERT_NE(encoded, nullptr); +} + +TEST_F(GltfEncoderTest, KhrTextureTransformWithoutFallback) { + // This is the example from Khronos, which should have "KHR_texture_transform" + // listed in the extensionsRequired, but does not for testing out client + // implementations. + const std::string filename = + "KhronosSampleModels/TextureTransformTest/glTF/TextureTransformTest.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have eight occurences of "KHR_materials_unlit". Six in the + // materials and one in extensionsUsed and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_texture_transform", + 8); + + // glTF file should still contain only two occurences of '"sampler": 0'. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "\"sampler\": 0", 2); + + // glTF file should have one occurence of "wrapS", "wrapT", "minFilter", and + // "magFilter". + CheckGltfFileAtLeastStringCount(output_gltf_filename, "wrapS", 1); + CheckGltfFileAtLeastStringCount(output_gltf_filename, "wrapT", 1); + CheckGltfFileAtLeastStringCount(output_gltf_filename, "minFilter", 1); + CheckGltfFileAtLeastStringCount(output_gltf_filename, "magFilter", 1); +} + +TEST_F(GltfEncoderTest, KhrTextureTransformWithoutFallbackRequried) { + // This is the example from Khronos, changed to list "KHR_texture_transform" + // in extensionsRequired. + const std::string filename = + "glTF/TextureTransformTestWithRequired/" + "TextureTransformTestWithRequired.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have eight occurences of "KHR_materials_unlit". Six in the + // materials and one in extensionsUsed and one in extensionsRequired. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_texture_transform", + 8); +} + +TEST_F(GltfEncoderTest, KhrTextureTransformWithFallback) { + // This is an example of "KHR_texture_transform" extension with fallback data. + const std::string filename = + "glTF/KhrTextureTransformWithFallback/" + "KhrTextureTransformWithFallback.gltf"; + const std::unique_ptr scene(DecodeTestGltfFileToScene(filename)); + ASSERT_NE(scene, nullptr); + + const std::string output_gltf_filename = + draco::GetTestTempFileFullPath("encoded_example.gltf"); + std::string output_gltf_dir; + std::string output_gltf_basename; + draco::SplitPath(output_gltf_filename, &output_gltf_dir, + &output_gltf_basename); + + GltfEncoder gltf_encoder; + ASSERT_TRUE(gltf_encoder.EncodeFile(*scene, output_gltf_filename).ok()) + << "Failed to encode glTF filename:" << output_gltf_filename; + // glTF file should have two occurences of "KHR_materials_unlit". One in the + // materials and one in extensionsUsed. + CheckGltfFileAtLeastStringCount(output_gltf_filename, "KHR_texture_transform", + 2); +} + +// Tests if the source file has a node with an identity matrix, that we do not +// output the identiy matrix. +TEST_F(GltfEncoderTest, MeshWithIdentityTransformation) { + const std::string gltf_source_full_path = + GetTestFileFullPath("Triangle/glTF/Triangle_identity_matrix.gltf"); + + // Check that the source file contains one "matrix" and no "translation" + // strings. + CheckGltfFileAtLeastStringCount(gltf_source_full_path, "matrix", 1); + CheckGltfFileNoString(gltf_source_full_path, "translation"); + + std::unique_ptr scene = draco::ReadSceneFromTestFile( + "Triangle/glTF/Triangle_identity_matrix.gltf"); + ASSERT_NE(scene, nullptr); + SceneNode *scene_node = scene->GetNode(SceneNodeIndex(0)); + ASSERT_NE(scene_node, nullptr); + const TrsMatrix &trs_matrix = scene_node->GetTrsMatrix(); + + // gltf_decoder will not set the trs matrix if the matrix is identity. + ASSERT_FALSE(trs_matrix.MatrixSet()); + + // Add the identity matrix. + TrsMatrix trsm; + trsm.SetMatrix(Eigen::Matrix4d::Identity()); + scene_node->SetTrsMatrix(trsm); + + const TrsMatrix &check_trs_matrix = scene_node->GetTrsMatrix(); + ASSERT_TRUE(check_trs_matrix.MatrixSet()); + ASSERT_EQ(check_trs_matrix.IsMatrixIdentity(), true); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("MeshWithIdentityTransformation.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + + ASSERT_TRUE(gltf_encoder.EncodeToFile( + *scene.get(), gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + std::unique_ptr scene_gltf = + std::move(ReadSceneFromFile(gltf_file_full_path)).value(); + ASSERT_NE(scene_gltf, nullptr); + // Check that the output file contains no "matrix" or "translation" strings. + CheckGltfFileNoString(gltf_file_full_path, "matrix"); + CheckGltfFileNoString(gltf_file_full_path, "translation"); +} + +// Tests if the source file has a node with a matrix that only has the +// translation values set. If it does then instead of outputting the full matrix +// we only output the "translation" glTF element. +TEST_F(GltfEncoderTest, MeshWithTranslationOnlyMatrix) { + std::unique_ptr scene = draco::ReadSceneFromTestFile( + "Triangle/glTF/Triangle_translation_only_matrix.gltf"); + ASSERT_NE(scene, nullptr); + SceneNode *scene_node = scene->GetNode(SceneNodeIndex(0)); + ASSERT_NE(scene_node, nullptr); + const TrsMatrix &input_trs_matrix = scene_node->GetTrsMatrix(); + ASSERT_TRUE(input_trs_matrix.MatrixSet()); + ASSERT_FALSE(input_trs_matrix.TranslationSet()); + ASSERT_FALSE(input_trs_matrix.RotationSet()); + ASSERT_FALSE(input_trs_matrix.ScaleSet()); + ASSERT_TRUE(input_trs_matrix.IsMatrixTranslationOnly()); + + const std::string gltf_file_full_path = + draco::GetTestTempFileFullPath("MeshWithTranslationOnlyMatrix.gltf"); + std::string folder_path; + std::string gltf_file_name; + draco::SplitPath(gltf_file_full_path, &folder_path, &gltf_file_name); + GltfEncoder gltf_encoder; + + ASSERT_TRUE(gltf_encoder.EncodeToFile( + *scene.get(), gltf_file_full_path, folder_path)) + << "Failed gltf_file_full_path:" << gltf_file_full_path + << " folder_path:" << folder_path; + std::unique_ptr scene_gltf = + std::move(ReadSceneFromFile(gltf_file_full_path)).value(); + ASSERT_NE(scene_gltf, nullptr); + SceneNode *output_scene_node = scene_gltf->GetNode(SceneNodeIndex(0)); + ASSERT_NE(output_scene_node, nullptr); + const TrsMatrix &output_trs_matrix = output_scene_node->GetTrsMatrix(); + ASSERT_FALSE(output_trs_matrix.MatrixSet()); + ASSERT_TRUE(output_trs_matrix.TranslationSet()); + ASSERT_FALSE(output_trs_matrix.RotationSet()); + ASSERT_FALSE(output_trs_matrix.ScaleSet()); +} + +// Tests that a scene can be encoded to buffer in GLB format. +TEST_F(GltfEncoderTest, EncodeToBuffer) { + // Load scene from file. + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene = ReadSceneFromTestFile(file_name); + ASSERT_NE(scene, nullptr); + + // Encode scene to buffer in GLB format. + GltfEncoder encoder; + EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeToBuffer(*scene, &buffer)); + ASSERT_NE(buffer.size(), 0); + + // Write scene to file in GLB format. + const std::string glb_file_path = draco::GetTestTempFileFullPath("temp.glb"); + std::string folder_path; + std::string glb_file_name; + draco::SplitPath(glb_file_path, &folder_path, &glb_file_name); + encoder.EncodeToFile(*scene, glb_file_path, folder_path); + + // Check that the buffer contents match the GLB file contents. + ASSERT_EQ(buffer.size(), draco::GetFileSize(glb_file_path)); + std::vector file_data; + ASSERT_TRUE(ReadFileToBuffer(glb_file_path, &file_data)); + ASSERT_EQ(std::memcmp(file_data.data(), buffer.data(), buffer.size()), 0); +} + +// Tests that a scene with lights can be encoded into a file. +TEST_F(GltfEncoderTest, EncodeLights) { + const std::string file_name = "sphere_lights.gltf"; + const std::unique_ptr scene = ReadSceneFromTestFile(file_name); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumLights(), 4); + EncodeSceneToGltfAndCompare(scene.get()); +} + +// Helper method for adding mesh group GPU instancing to the milk truck scene. +draco::Status AddGpuInstancingToMilkTruck(draco::Scene *scene) { + // Create an instance and set its transformation TRS vectors. + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(Eigen::Vector3d(-0.2, 0.0, 0.0)); + instance_0.trs.SetScale(Eigen::Vector3d(1.0, 1.0, 1.0)); + + // Create another instance. + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(Eigen::Vector3d(1.0, 0.0, 0.0)); + instance_1.trs.SetScale(Eigen::Vector3d(2.0, 2.0, 2.0)); + + // Add an empty GPU instancing object to the scene. + const draco::InstanceArrayIndex index = scene->AddInstanceArray(); + draco::InstanceArray *gpu_instancing = scene->GetInstanceArray(index); + + // Add two instances to the GPU instancing object stored in the scene. + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_0)); + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_1)); + + // Assign the GPU instancing object to two mesh groups in two scene nodes. + scene->GetNode(draco::SceneNodeIndex(2))->SetInstanceArrayIndex(index); + scene->GetNode(draco::SceneNodeIndex(4))->SetInstanceArrayIndex(index); + + return draco::OkStatus(); +} + +// Tests that a scene with instance arrays can be encoded into a file. Decoder +// has no GPU instancing support, so we will compare encoded file to a golden +// file. +TEST_F(GltfEncoderTest, EncodeInstanceArrays) { + // Read the milk truck. + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + + // Add GPU instancing to the scene for testing. + DRACO_ASSERT_OK(AddGpuInstancingToMilkTruck(scene.get())); + ASSERT_EQ(scene->NumInstanceArrays(), 1); + ASSERT_EQ(scene->NumNodes(), 5); + + // Prepare file paths. + const std::string temp_path = draco::GetTestTempFileFullPath("Truck.glb"); + const std::string golden_path = + GetTestFileFullPath("CesiumRowingTruckWithGpuInstancing.glb"); + + // Encode scene to a temporary file in GLB format. + std::string folder; + std::string name; + draco::SplitPath(temp_path, &folder, &name); + GltfEncoder encoder; + ASSERT_TRUE(encoder.EncodeToFile(*scene, temp_path, folder)) + << "Failed to encode to temporary file:" << temp_path; + + // Read encoded file to buffer. + std::vector encoded_data; + ASSERT_TRUE(ReadFileToBuffer(temp_path, &encoded_data)); +} + +// Tests that a scene with materials variants can be encoded into a file. +TEST_F(GltfEncoderTest, EncodeMaterialsVariants) { + const std::string file_name = + "KhronosSampleModels/DragonAttenuation/glTF/DragonAttenuation.gltf"; + const std::unique_ptr scene = ReadSceneFromTestFile(file_name); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterialsVariants(), 2); + EncodeSceneToGltfAndCompare(scene.get()); +} + +// Tests encoding of draco::Scene to glTF with various mesh feature ID sets and +// structural metadata property table. +TEST_F(GltfEncoderTest, EncodeSceneWithMeshFeaturesWithStructuralMetadata) { + const std::string file_name = "BoxMeta/glTF/BoxMeta.gltf"; + constexpr bool kHasMeshFeatures = true; + constexpr bool kHasStructuralMetadata = true; + constexpr bool kHasDracoCompression = false; + + // Read test file from file. + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Encode the scene to glTF and decode it back to draco::Scene and check. + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + ASSERT_NE(scene_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene_from_gltf, + kHasDracoCompression); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*scene_from_gltf); +} + +// Tests encoding of draco::Scene with Draco compression to glTF with various +// mesh feature ID sets. +TEST_F(GltfEncoderTest, EncodeSceneWithMeshFeaturesWithDracoCompression) { + const std::string file_name = "BoxMetaDraco/glTF/BoxMetaDraco.gltf"; + constexpr bool kHasMeshFeatures = true; + constexpr bool kHasStructuralMetadata = false; + constexpr bool kHasDracoCompression = true; + + // Read test file from file. + const std::unique_ptr scene(DecodeTestGltfFileToScene(file_name)); + ASSERT_NE(scene, nullptr); + + // Encode the scene to glTF and decode it back to draco::Scene and check. + std::unique_ptr scene_from_gltf; + SceneToDecodedGltfScene(*scene, &scene_from_gltf); + ASSERT_NE(scene_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*scene_from_gltf, + kHasDracoCompression); +} + +// Tests encoding of draco::Mesh to glTF with various mesh feature ID sets and +// structural metadata property table. +TEST_F(GltfEncoderTest, EncodeMeshWithMeshFeaturesWithStructuralMetadata) { + const std::string file_name = "BoxMeta/glTF/BoxMeta.gltf"; + constexpr bool kHasDracoCompression = false; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + + // Encode the scene to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh_from_gltf, + kHasDracoCompression); + GltfTestHelper::CheckBoxMetaStructuralMetadata(*mesh_from_gltf); +} + +// Tests encoding of draco::Mesh with Draco compression to glTF with various +// mesh feature ID sets. +TEST_F(GltfEncoderTest, EncodeMeshWithMeshFeaturesWithDracoCompression) { + constexpr bool kHasDracoCompression = true; + const std::string file_name = "BoxMetaDraco/glTF/BoxMetaDraco.gltf"; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + + // Encode the scene to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + GltfTestHelper::CheckBoxMetaMeshFeatures(*mesh_from_gltf, + kHasDracoCompression); +} + +// Tests encoding of draco::Mesh with mesh features associated with different +// mesh primitives. +TEST_F(GltfEncoderTest, EncodeMeshWithMeshFeaturesWithMultiplePrimitives) { + const std::string file_name = "BoxesMeta/glTF/BoxesMeta.gltf"; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + // All mesh features should share two textures. + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // Encode the scene to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + + ASSERT_EQ(mesh_from_gltf->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(mesh_from_gltf->NumMeshFeatures(), 5); + + // First two mesh features should be used by material 0 and the reamining by + // material 1. + for (draco::MeshFeaturesIndex mfi(0); mfi < 5; ++mfi) { + // Each mesh feature should be used by a single material. + ASSERT_EQ(mesh_from_gltf->NumMeshFeaturesMaterialMasks(mfi), 1); + if (mfi.value() < 2) { + ASSERT_EQ(mesh_from_gltf->GetMeshFeaturesMaterialMask(mfi, 0), 0); + } else { + ASSERT_EQ(mesh_from_gltf->GetMeshFeaturesMaterialMask(mfi, 0), 1); + } + } + // All mesh features should share two textures. + ASSERT_EQ(mesh_from_gltf->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // Ensure it still works correctly when we re-encode the source |mesh| as a + // scene. + std::unique_ptr scene_from_gltf; + MeshToDecodedGltfScene(*mesh, &scene_from_gltf); + ASSERT_NE(scene_from_gltf, nullptr); + + ASSERT_EQ(scene_from_gltf->NumMeshes(), 2); + + // First mesh should have 2 mesh features and the other one 3 mesh features. + ASSERT_EQ(scene_from_gltf->GetMesh(draco::MeshIndex(0)).NumMeshFeatures(), 2); + ASSERT_EQ(scene_from_gltf->GetMesh(draco::MeshIndex(1)).NumMeshFeatures(), 3); + + // All mesh features should share two textures. + ASSERT_EQ(scene_from_gltf->GetNonMaterialTextureLibrary().NumTextures(), 2); +} + +// Tests encoding of draco::Mesh containing a point cloud and two materials. +TEST_F(GltfEncoderTest, EncodePointCloudWithMaterials) { + const std::string file_name = + "SphereTwoMaterials/sphere_two_materials_point_cloud.gltf"; + + // Read test file from file. + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh, nullptr); + + // Input should have no faces. + ASSERT_EQ(mesh->num_faces(), 0); + + // There should be two materials + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + + // Encode the mesh to glTF and decode it back to draco::Mesh and check. + std::unique_ptr mesh_from_gltf; + MeshToDecodedGltfMesh(*mesh, &mesh_from_gltf); + ASSERT_NE(mesh_from_gltf, nullptr); + + ASSERT_EQ(mesh_from_gltf->num_faces(), 0); + ASSERT_EQ(mesh_from_gltf->GetMaterialLibrary().NumMaterials(), 2); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_test_helper.cc b/contrib/draco/src/draco/io/gltf_test_helper.cc new file mode 100644 index 000000000..13cce6f4e --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_test_helper.cc @@ -0,0 +1,823 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_test_helper.h" + +#include +#include +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/metadata/property_table.h" +#include "draco/texture/texture_library.h" + +namespace draco { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +void GltfTestHelper::AddBoxMetaMeshFeatures(Scene *scene) { + // Check the scene. + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 1); + TextureLibrary &texture_library = scene->GetNonMaterialTextureLibrary(); + ASSERT_EQ(texture_library.NumTextures(), 0); + + // Check the mesh. + Mesh &mesh = scene->GetMesh(MeshIndex(0)); + ASSERT_EQ(mesh.num_faces(), 12); + ASSERT_EQ(mesh.num_attributes(), 2); + ASSERT_EQ(mesh.num_points(), 24); + + // Get mesh element counts. + const int num_faces = mesh.num_faces(); + const int num_corners = 3 * mesh.num_faces(); + const int num_vertices = + mesh.GetNamedAttribute(GeometryAttribute::POSITION)->size(); + + // Add feature ID set with per-face Uint8 attribute named _FEATURE_ID_0. + { + // Create feature ID attribute. + constexpr DataType kType = DataType::DT_UINT8; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::GENERIC, 1, kType, false, mesh.num_faces()); + for (AttributeValueIndex avi(0); avi < num_faces; ++avi) { + const int8_t val = avi.value(); + pa->SetAttributeValue(avi, &val); + } + const int att_id = mesh.AddPerFaceAttribute(std::move(pa)); + std::unique_ptr metadata(new AttributeMetadata()); + metadata->AddEntryString("attribute_name", "_FEATURE_ID_0"); + mesh.AddAttributeMetadata(att_id, std::move(metadata)); + + // Add feature ID set to the mesh. + std::unique_ptr features(new MeshFeatures()); + features->SetLabel("faces"); + features->SetFeatureCount(num_faces); + features->SetNullFeatureId(100); + features->SetPropertyTableIndex(0); + features->SetAttributeIndex(0); + mesh.AddMeshFeatures(std::move(features)); + } + + // Add feature ID set with per-vertex Uint16 attribute named _FEATURE_ID_1. + { + // Create feature ID attribute. + constexpr DataType kType = DataType::DT_UINT16; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::GENERIC, 1, kType, false, num_vertices); + for (AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const uint16_t val = avi.value(); + pa->SetAttributeValue(avi, &val); + } + const int att_id = mesh.AddPerVertexAttribute(std::move(pa)); + std::unique_ptr metadata(new AttributeMetadata()); + metadata->AddEntryString("attribute_name", "_FEATURE_ID_1"); + mesh.AddAttributeMetadata(att_id, std::move(metadata)); + + // Add feature ID set to the mesh. + std::unique_ptr features(new MeshFeatures()); + features->SetLabel("vertices"); + features->SetFeatureCount(num_vertices); + features->SetNullFeatureId(101); + features->SetPropertyTableIndex(1); + features->SetAttributeIndex(1); + mesh.AddMeshFeatures(std::move(features)); + } + + // Add feature ID set with per-corner Float attribute named _FEATURE_ID_2. + { + // Create feature ID attribute. + constexpr DataType kType = DataType::DT_FLOAT32; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::GENERIC, 1, kType, false, num_corners); + IndexTypeVector corner_to_value( + num_corners); + for (AttributeValueIndex avi(0); avi < num_corners; ++avi) { + const float val = avi.value(); + pa->SetAttributeValue(avi, &val); + corner_to_value[CornerIndex(avi.value())] = avi; + } + const int att_id = + mesh.AddAttributeWithConnectivity(std::move(pa), corner_to_value); + std::unique_ptr metadata(new AttributeMetadata()); + metadata->AddEntryString("attribute_name", "_FEATURE_ID_2"); + mesh.AddAttributeMetadata(att_id, std::move(metadata)); + + // Add feature ID set to the mesh. + std::unique_ptr features(new MeshFeatures()); + features->SetFeatureCount(num_corners); + features->SetAttributeIndex(2); + mesh.AddMeshFeatures(std::move(features)); + } + + // Add feature ID set with the IDs stored in the R texture channel and + // accessible via the first texture coordinate attribute. + { + // Add the first texture coordinate attribute. + constexpr DataType kType = DataType::DT_FLOAT32; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::TEX_COORD, 2, kType, false, num_vertices); + std::vector> uv = { + {0.0000f, 0.0000f}, {0.0000f, 0.5000f}, {0.0000f, 1.0000f}, + {0.5000f, 0.0000f}, {0.5000f, 0.5000f}, {0.5000f, 1.0000f}, + {1.0000f, 0.0000f}, {1.0000f, 0.5000f}}; + for (AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const int index = avi.value(); + pa->SetAttributeValue(avi, uv[index].data()); + } + mesh.AddPerVertexAttribute(std::move(pa)); + } + + // Add feature ID set with the IDs stored in the GBA texture channels and + // accessible via the second texture coordinate attribute. + { + // Add the second texture coordinate attribute. + constexpr DataType kType = DataType::DT_FLOAT32; + std::unique_ptr pa(new PointAttribute()); + pa->Init(GeometryAttribute::TEX_COORD, 2, kType, false, num_vertices); + std::vector> uv = { + {0.0000f, 0.0000f}, {0.0000f, 0.5000f}, {0.0000f, 1.0000f}, + {0.5000f, 0.0000f}, {0.5000f, 0.5000f}, {0.5000f, 1.0000f}, + {1.0000f, 0.0000f}, {1.0000f, 0.5000f}}; + for (AttributeValueIndex avi(0); avi < num_vertices; ++avi) { + const int index = avi.value(); + pa->SetAttributeValue(avi, uv[index].data()); + } + mesh.AddPerVertexAttribute(std::move(pa)); + ASSERT_EQ(mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + } +} + +void GltfTestHelper::AddBoxMetaStructuralMetadata(Scene *scene) { + // Add structural metadata property table schema in the following JSON: + // "schema": { + // "id": "galaxy", + // "classes": { + // "planet": { + // "properties": { + // "color": { + // "componentType": "UINT8", + // "description": "The RGB color.", + // "required": true, + // "type": "VEC3" + // }, + // "name": { + // "description": "The name.", + // "required": true, + // "type": "STRING" + // } + // "sequence": { + // "description": "The number sequence.", + // "required": false, + // "type": "SCALAR" + // } + // } + // } + // }, + // "enums": { + // "classifications": { + // "description": "Classifications of planets.", + // "name": "classifications", + // "values": [ + // { "name": "Unspecified", "value": 0 }, + // { "name": "Gas Giant", "value": 1 }, + // { "name": "Waterworld", "value": 2 }, + // { "name": "Agriworld", "value": 3 }, + // { "name": "Ordnance", "value": 4 } + // ] + // } + // } + // } + typedef PropertyTable::Schema::Object Object; + PropertyTable::Schema schema; + Object &json = schema.json; + json.SetObjects().emplace_back("id", "galaxy"); + json.SetObjects().emplace_back("classes"); + json.SetObjects().back().SetObjects().emplace_back("planet"); + Object &planet = json.SetObjects().back().SetObjects().back(); + planet.SetObjects().emplace_back("properties"); + Object &properties = planet.SetObjects().back(); + + properties.SetObjects().emplace_back("color"); + Object &color = properties.SetObjects().back(); + color.SetObjects().emplace_back("componentType", "UINT8"); + color.SetObjects().emplace_back("description", "The RGB color."); + color.SetObjects().emplace_back("required", true); + color.SetObjects().emplace_back("type", "VEC3"); + + properties.SetObjects().emplace_back("name"); + Object &name = properties.SetObjects().back(); + name.SetObjects().emplace_back("description", "The name."); + name.SetObjects().emplace_back("required", true); + name.SetObjects().emplace_back("type", "STRING"); + + properties.SetObjects().emplace_back("sequence"); + Object &sequence = properties.SetObjects().back(); + sequence.SetObjects().emplace_back("description", "The number sequence."); + sequence.SetObjects().emplace_back("required", false); + sequence.SetObjects().emplace_back("type", "SCALAR"); + + json.SetObjects().emplace_back("enums"); + json.SetObjects().back().SetObjects().emplace_back("classifications"); + Object &classifications = json.SetObjects().back().SetObjects().back(); + classifications.SetObjects().emplace_back("description", + "Classifications of planets."); + classifications.SetObjects().emplace_back("name", "classifications"); + classifications.SetObjects().emplace_back("values"); + Object &values = classifications.SetObjects().back(); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Unspecified"); + values.SetArray().back().SetObjects().emplace_back("value", 0); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Gas Giant"); + values.SetArray().back().SetObjects().emplace_back("value", 1); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Waterworld"); + values.SetArray().back().SetObjects().emplace_back("value", 2); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Agriworld"); + values.SetArray().back().SetObjects().emplace_back("value", 3); + + values.SetArray().emplace_back(); + values.SetArray().back().SetObjects().emplace_back("name", "Ordnance"); + values.SetArray().back().SetObjects().emplace_back("value", 4); + + // Add property table schema to the scene. + scene->GetStructuralMetadata().SetPropertyTableSchema(schema); + + // Add structural metadata property table. + std::unique_ptr table(new PropertyTable()); + table->SetName("Galaxy far far away."); + table->SetClass("planet"); + table->SetCount(16); + + // Add property describing RGB color components of the planet class. + { + std::unique_ptr property( + new PropertyTable::Property()); + property->SetName("color"); + property->GetData().target = 34962; // ARRAY_BUFFER. + property->GetData().data = {94, 94, 194, // Tatooine + 94, 145, 161, // Corusant + 118, 171, 91, // Naboo + 103, 139, 178, // Alderaan + 83, 98, 154, // Dagobah + 91, 177, 175, // Mandalore + 190, 92, 108, // Corellia + 72, 69, 169, // Kamino + 154, 90, 101, // Kashyyyk + 174, 85, 175, // Dantooine + 184, 129, 96, // Hoth + 185, 91, 180, // Mustafar + 194, 150, 83, // Bespin + 204, 111, 134, // Yavin + 182, 90, 89, // Geonosis + 0, 0, 0}; // UNLABELED + table->AddProperty(std::move(property)); + } + + // Add property that describes names of the planet class. + { + std::unique_ptr property( + new PropertyTable::Property()); + property->SetName("name"); + property->GetData().target = 34963; // ELEMENT_ARRAY_BUFFER. + const std::string data = + "named_class:Tatooine" + "named_class:Corusant" + "named_class:Naboo" + "named_class:Alderaan" + "named_class:Dagobah" + "named_class:Mandalore" + "named_class:Corellia" + "named_class:Kamino" + "named_class:Kashyyyk" + "named_class:Dantooine" + "named_class:Hoth" + "named_class:Mustafar" + "named_class:Bespin" + "named_class:Yavin" + "named_class:Geonosis" + "UNLABELED"; + property->GetData().data.assign(data.begin(), data.end()); + property->GetStringOffsets().type = "UINT32"; + property->GetStringOffsets().data.target = 34963; // ELEMENT_ARRAY_BUFFER. + property->GetStringOffsets().data.data = {0, 0, 0, 0, // Tatooine + 20, 0, 0, 0, // Corusant + 40, 0, 0, 0, // Naboo + 57, 0, 0, 0, // Alderaan + 77, 0, 0, 0, // Dagobah + 96, 0, 0, 0, // Mandalore + 117, 0, 0, 0, // Corellia + 137, 0, 0, 0, // Kamino + 155, 0, 0, 0, // Kashyyyk + 175, 0, 0, 0, // Dantooine + 196, 0, 0, 0, // Hoth + 212, 0, 0, 0, // Mustafar + 232, 0, 0, 0, // Bespin + 250, 0, 0, 0, // Yavin + 12, 1, 0, 0, // Geonosis + 32, 1, 0, 0, // UNLABELED + 41, 1, 0, 0}; + table->AddProperty(std::move(property)); + } + + // Add property that contains variable-length number sequence of the planet + // class. + { + std::unique_ptr property( + new PropertyTable::Property()); + property->SetName("sequence"); + property->GetData().target = 34963; // ELEMENT_ARRAY_BUFFER. + const std::vector data = { + 0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f, // Tatooine + 6.5f, 7.5f, // Corusant + 8.5f, // Naboo + 9.5f, // Alderaan + 10.5f, 11.5f, // Dagobah + 12.5f, 13.5f, 14.5f, 15.5f, // Mandalore + 16.5f, 17.5f, // Corellia + 18.5f, 19.5f, // Kamino + 20.5f, 21.5f, 22.5f, // Kashyyyk + 23.5f, 24.5f, 25.5f, // Dantooine + 26.5f, 27.5f, // Hoth + 28.5f, 29.5f, // Mustafar + 30.5f, 31.5f, 32.5f, // Bespin + 33.5f, 34.5f, 35.5f, // Yavin + 36.5f, 37.5f, 38.5f, 39.5f, 40.5f // Geonosis + }; // UNLABELED (empty array). + property->GetData().data.resize(4 * data.size()); + memcpy(property->GetData().data.data(), data.data(), 4 * data.size()); + property->GetArrayOffsets().type = "UINT8"; + property->GetArrayOffsets().data.target = 34963; // ELEMENT_ARRAY_BUFFER. + property->GetArrayOffsets().data.data = { + 0 * 4, // Tatooine + 6 * 4, // Corusant + 8 * 4, // Naboo + 9 * 4, // Alderaan + 10 * 4, // Dagobah + 12 * 4, // Mandalore + 16 * 4, // Corellia + 18 * 4, // Kamino + 20 * 4, // Kashyyyk + 23 * 4, // Dantooine + 26 * 4, // Hoth + 28 * 4, // Mustafar + 30 * 4, // Bespin + 33 * 4, // Yavin + 36 * 4, // Geonosis + 41 * 4, // UNLABELED (empty array). + 41 * 4}; + table->AddProperty(std::move(property)); + } + + // Add property table to the scene. + scene->GetStructuralMetadata().AddPropertyTable(std::move(table)); +} + +template <> +void GltfTestHelper::CheckBoxMetaMeshFeatures(const Mesh &geometry, + bool has_draco_compression) { + CheckBoxMetaMeshFeatures(geometry, geometry.GetNonMaterialTextureLibrary(), + has_draco_compression); +} + +template <> +void GltfTestHelper::CheckBoxMetaMeshFeatures(const Scene &geometry, + bool has_draco_compression) { + ASSERT_EQ(geometry.NumMeshes(), 1); + CheckBoxMetaMeshFeatures(geometry.GetMesh(MeshIndex(0)), + geometry.GetNonMaterialTextureLibrary(), + has_draco_compression); +} + +void GltfTestHelper::CheckBoxMetaMeshFeatures(const Mesh &mesh, + const TextureLibrary &texture_lib, + bool has_draco_compression) { + // Check texture library. + ASSERT_EQ(texture_lib.NumTextures(), 2); + + // Check basic mesh properties. + ASSERT_EQ(mesh.NumMeshFeatures(), 5); + ASSERT_EQ(mesh.num_faces(), 12); + ASSERT_EQ(mesh.num_attributes(), 7); + ASSERT_EQ(mesh.num_points(), 36); + ASSERT_EQ(mesh.NumNamedAttributes(GeometryAttribute::GENERIC), 3); + ASSERT_EQ(mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD), 2); + + // Get mesh element counts. + const int num_faces = mesh.num_faces(); + const int num_corners = 3 * mesh.num_faces(); + const int num_vertices = + mesh.GetNamedAttribute(GeometryAttribute::POSITION)->size(); + + // Check mesh feature ID set at index 0. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(0)); + ASSERT_EQ(features.GetLabel(), "faces"); + ASSERT_EQ(features.GetFeatureCount(), num_faces); + ASSERT_EQ(features.GetNullFeatureId(), 100); + ASSERT_EQ(features.GetPropertyTableIndex(), 0); + ASSERT_EQ(features.GetAttributeIndex(), 0); + ASSERT_TRUE(features.GetTextureChannels().empty()); + ASSERT_EQ(features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(features.GetTextureMap().tex_coord_index(), -1); + + // Check per-face Uint8 attribute named _FEATURE_ID_0. + const int att_id = + mesh.GetAttributeIdByMetadataEntry("attribute_name", "_FEATURE_ID_0"); + auto att = mesh.GetAttributeByUniqueId(att_id); + ASSERT_NE(att, nullptr); + ASSERT_EQ(att->attribute_type(), GeometryAttribute::GENERIC); + ASSERT_EQ(att->data_type(), DataType::DT_UINT8); + ASSERT_EQ(att->num_components(), 1); + ASSERT_EQ(att->size(), num_faces); + ASSERT_EQ(att->indices_map_size(), num_corners); + + // Check that the values are all the numbers from 0 to 12. + const std::vector expected_values = + has_draco_compression + ? std::vector{7, 11, 10, 3, 2, 5, 4, 1, 6, 9, 8, 0} + : std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + for (int i = 0; i < num_faces; i++) { + uint8_t val; + att->GetValue(AttributeValueIndex(i), &val); + ASSERT_EQ(val, expected_values[i]); + } + + // Check that the corners of each face have a common value. + for (int i = 0; i < num_faces; i++) { + const auto face = mesh.face(FaceIndex(i)); + ASSERT_EQ(*att->GetAddressOfMappedIndex(face[0]), + *att->GetAddressOfMappedIndex(face[1])); + ASSERT_EQ(*att->GetAddressOfMappedIndex(face[0]), + *att->GetAddressOfMappedIndex(face[2])); + } + } + + // Check the 2nd mesh feature ID set at index 1. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(1)); + ASSERT_EQ(features.GetLabel(), "vertices"); + ASSERT_EQ(features.GetFeatureCount(), num_vertices); + ASSERT_EQ(features.GetNullFeatureId(), 101); + ASSERT_EQ(features.GetPropertyTableIndex(), 1); + ASSERT_EQ(features.GetAttributeIndex(), 1); + ASSERT_TRUE(features.GetTextureChannels().empty()); + ASSERT_EQ(features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(features.GetTextureMap().tex_coord_index(), -1); + + // Check per-vertex Uint16 attribute named _FEATURE_ID_1. + const int att_id = + mesh.GetAttributeIdByMetadataEntry("attribute_name", "_FEATURE_ID_1"); + auto att = mesh.GetAttributeByUniqueId(att_id); + ASSERT_NE(att, nullptr); + ASSERT_EQ(att->attribute_type(), GeometryAttribute::GENERIC); + ASSERT_EQ(att->data_type(), DataType::DT_UINT16); + ASSERT_EQ(att->num_components(), 1); + ASSERT_EQ(att->size(), num_vertices); + ASSERT_EQ(att->indices_map_size(), num_corners); + + // Check that the values are all the numbers from 0 to 7. + const std::vector expected_values = + has_draco_compression ? std::vector{3, 6, 7, 4, 5, 0, 1, 2} + : std::vector{0, 1, 2, 3, 4, 5, 6, 7}; + for (int i = 0; i < num_vertices; i++) { + uint16_t val; + att->GetValue(AttributeValueIndex(i), &val); + ASSERT_EQ(val, expected_values[i]); + } + + // Check that the corners of a face have unique values. + for (int i = 0; i < num_faces; i++) { + const auto face = mesh.face(FaceIndex(i)); + ASSERT_NE(*att->GetAddressOfMappedIndex(face[0]), + *att->GetAddressOfMappedIndex(face[1])); + ASSERT_NE(*att->GetAddressOfMappedIndex(face[1]), + *att->GetAddressOfMappedIndex(face[2])); + ASSERT_NE(*att->GetAddressOfMappedIndex(face[2]), + *att->GetAddressOfMappedIndex(face[0])); + } + } + + // Check the 3rd mesh feature ID set at index 2. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(2)); + ASSERT_TRUE(features.GetLabel().empty()); + ASSERT_EQ(features.GetFeatureCount(), num_corners); + ASSERT_EQ(features.GetNullFeatureId(), -1); + ASSERT_EQ(features.GetPropertyTableIndex(), -1); + ASSERT_EQ(features.GetAttributeIndex(), 2); + ASSERT_TRUE(features.GetTextureChannels().empty()); + ASSERT_EQ(features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(features.GetTextureMap().tex_coord_index(), -1); + + // Check per-corner Float attribute named _FEATURE_ID_2. + const int att_id = + mesh.GetAttributeIdByMetadataEntry("attribute_name", "_FEATURE_ID_2"); + auto att = mesh.GetAttributeByUniqueId(att_id); + ASSERT_NE(att, nullptr); + ASSERT_EQ(att->attribute_type(), GeometryAttribute::GENERIC); + ASSERT_EQ(att->data_type(), DataType::DT_FLOAT32); + ASSERT_EQ(att->num_components(), 1); + ASSERT_EQ(att->size(), num_corners); + ASSERT_EQ(att->indices_map_size(), 0); + ASSERT_TRUE(att->is_mapping_identity()); + + // Check that the values are from 0 to 35. + const std::vector expected_values = + has_draco_compression + ? std::vector{23, 21, 22, 33, 34, 35, 31, 32, 30, 9, 10, 11, + 7, 8, 6, 15, 16, 17, 14, 12, 13, 5, 3, 4, + 19, 20, 18, 27, 28, 29, 26, 24, 25, 1, 2, 0} + : std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35}; + for (int i = 0; i < num_corners; i++) { + float val; + att->GetValue(AttributeValueIndex(i), &val); + ASSERT_EQ(val, expected_values[i]); + } + + // Check that the corners have unique values. + for (int i = 0; i < num_faces; i++) { + const auto face = mesh.face(FaceIndex(i)); + float v0, v1, v2; + att->GetMappedValue(face[0], &v0); + att->GetMappedValue(face[1], &v1); + att->GetMappedValue(face[2], &v2); + ASSERT_EQ(v0, expected_values[3 * i + 0]); + ASSERT_EQ(v1, expected_values[3 * i + 1]); + ASSERT_EQ(v2, expected_values[3 * i + 2]); + } + } + + // Check mesh feature ID set at index 3. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(3)); + ASSERT_TRUE(features.GetLabel().empty()); + ASSERT_EQ(features.GetFeatureCount(), 6); + ASSERT_EQ(features.GetNullFeatureId(), -1); + ASSERT_EQ(features.GetPropertyTableIndex(), -1); + ASSERT_EQ(features.GetAttributeIndex(), -1); + } + + // Check mesh feature ID set at index 4. + { + // Check mesh features. + const MeshFeatures &features = mesh.GetMeshFeatures(MeshFeaturesIndex(4)); + ASSERT_EQ(features.GetLabel(), "water"); + ASSERT_EQ(features.GetFeatureCount(), 2); + ASSERT_EQ(features.GetNullFeatureId(), -1); + ASSERT_EQ(features.GetPropertyTableIndex(), -1); + ASSERT_EQ(features.GetAttributeIndex(), -1); + } +} + +void GltfTestHelper::CheckBoxMetaStructuralMetadata( + const StructuralMetadata &structural_metadata) { + // Check property table schema. + { + const PropertyTable::Schema &schema = + structural_metadata.GetPropertyTableSchema(); + ASSERT_FALSE(schema.Empty()); + const PropertyTable::Schema::Object &json = schema.json; + ASSERT_EQ(json.GetObjects().size(), 3); + ASSERT_EQ(json.GetObjects()[0].GetName(), "classes"); + ASSERT_EQ(json.GetObjects()[0].GetObjects().size(), 1); + ASSERT_EQ(json.GetObjects()[0].GetObjects()[0].GetName(), "planet"); + ASSERT_EQ(json.GetObjects()[0].GetObjects()[0].GetObjects().size(), 1); + + const auto &properties = + json.GetObjects()[0].GetObjects()[0].GetObjects()[0]; + ASSERT_EQ(properties.GetName(), "properties"); + ASSERT_EQ(properties.GetObjects().size(), 3); + + const auto &color = properties.GetObjects()[0]; + ASSERT_EQ(color.GetName(), "color"); + ASSERT_EQ(color.GetObjects().size(), 4); + ASSERT_EQ(color.GetObjects()[0].GetName(), "componentType"); + ASSERT_EQ(color.GetObjects()[1].GetName(), "description"); + ASSERT_EQ(color.GetObjects()[2].GetName(), "required"); + ASSERT_EQ(color.GetObjects()[3].GetName(), "type"); + ASSERT_EQ(color.GetObjects()[0].GetString(), "UINT8"); + ASSERT_EQ(color.GetObjects()[1].GetString(), "The RGB color."); + ASSERT_TRUE(color.GetObjects()[2].GetBoolean()); + ASSERT_EQ(color.GetObjects()[3].GetString(), "VEC3"); + + const auto &name = properties.GetObjects()[1]; + ASSERT_EQ(name.GetName(), "name"); + ASSERT_EQ(name.GetObjects().size(), 3); + ASSERT_EQ(name.GetObjects()[0].GetName(), "description"); + ASSERT_EQ(name.GetObjects()[1].GetName(), "required"); + ASSERT_EQ(name.GetObjects()[2].GetName(), "type"); + ASSERT_EQ(name.GetObjects()[0].GetString(), "The name."); + ASSERT_TRUE(name.GetObjects()[1].GetBoolean()); + ASSERT_EQ(name.GetObjects()[2].GetString(), "STRING"); + + const auto &sequence = properties.GetObjects()[2]; + ASSERT_EQ(sequence.GetName(), "sequence"); + ASSERT_EQ(sequence.GetObjects().size(), 3); + ASSERT_EQ(sequence.GetObjects()[0].GetName(), "description"); + ASSERT_EQ(sequence.GetObjects()[1].GetName(), "required"); + ASSERT_EQ(sequence.GetObjects()[2].GetName(), "type"); + ASSERT_EQ(sequence.GetObjects()[0].GetString(), "The number sequence."); + ASSERT_FALSE(sequence.GetObjects()[1].GetBoolean()); + ASSERT_EQ(sequence.GetObjects()[2].GetString(), "SCALAR"); + + ASSERT_EQ(json.GetObjects()[1].GetName(), "enums"); + const auto &classifications = json.GetObjects()[1].GetObjects()[0]; + ASSERT_EQ(classifications.GetName(), "classifications"); + ASSERT_EQ(classifications.GetObjects()[0].GetName(), "description"); + ASSERT_EQ(classifications.GetObjects()[0].GetString(), + "Classifications of planets."); + ASSERT_EQ(classifications.GetObjects()[1].GetName(), "name"); + ASSERT_EQ(classifications.GetObjects()[1].GetString(), "classifications"); + ASSERT_EQ(classifications.GetObjects()[2].GetName(), "values"); + const auto &values = classifications.GetObjects()[2]; + ASSERT_EQ(values.GetArray()[0].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[1].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[2].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[3].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[4].GetObjects()[0].GetName(), "name"); + ASSERT_EQ(values.GetArray()[0].GetObjects()[0].GetString(), "Unspecified"); + ASSERT_EQ(values.GetArray()[1].GetObjects()[0].GetString(), "Gas Giant"); + ASSERT_EQ(values.GetArray()[2].GetObjects()[0].GetString(), "Waterworld"); + ASSERT_EQ(values.GetArray()[3].GetObjects()[0].GetString(), "Agriworld"); + ASSERT_EQ(values.GetArray()[4].GetObjects()[0].GetString(), "Ordnance"); + ASSERT_EQ(values.GetArray()[0].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[1].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[2].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[3].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[4].GetObjects()[1].GetName(), "value"); + ASSERT_EQ(values.GetArray()[0].GetObjects()[1].GetInteger(), 0); + ASSERT_EQ(values.GetArray()[1].GetObjects()[1].GetInteger(), 1); + ASSERT_EQ(values.GetArray()[2].GetObjects()[1].GetInteger(), 2); + ASSERT_EQ(values.GetArray()[3].GetObjects()[1].GetInteger(), 3); + ASSERT_EQ(values.GetArray()[4].GetObjects()[1].GetInteger(), 4); + + ASSERT_EQ(json.GetObjects()[2].GetName(), "id"); + ASSERT_EQ(json.GetObjects()[2].GetString(), "galaxy"); + } + + // Check property table. + constexpr int kRows = 16; + ASSERT_EQ(structural_metadata.NumPropertyTables(), 1); + const PropertyTable &table = structural_metadata.GetPropertyTable(0); + ASSERT_EQ(table.GetName(), "Galaxy far far away."); + ASSERT_EQ(table.GetClass(), "planet"); + ASSERT_EQ(table.GetCount(), kRows); + ASSERT_EQ(table.NumProperties(), 3); + + // Check property that describes RGB color components of the planet class. + { + const PropertyTable::Property &property = table.GetProperty(0); + ASSERT_EQ(property.GetName(), "color"); + + ASSERT_EQ(property.GetData().data.size(), kRows * 3); // RGB components. + ASSERT_EQ(property.GetData().target, 34962); // ARRAY_BUFFER. + + ASSERT_EQ(property.GetData().data[0], 94); // Tatooine [94, 94, 194]. + ASSERT_EQ(property.GetData().data[1], 94); + ASSERT_EQ(property.GetData().data[2], 194); + ASSERT_EQ(property.GetData().data[18], 190); // Corellia [190, 92, 108]. + ASSERT_EQ(property.GetData().data[19], 92); + ASSERT_EQ(property.GetData().data[20], 108); + ASSERT_EQ(property.GetData().data[45], 0); // UNLABELED [0, 0, 0]. + ASSERT_EQ(property.GetData().data[46], 0); + ASSERT_EQ(property.GetData().data[47], 0); + + ASSERT_TRUE(property.GetArrayOffsets().type.empty()); + ASSERT_TRUE(property.GetArrayOffsets().data.data.empty()); + ASSERT_EQ(property.GetArrayOffsets().data.target, 0); + ASSERT_TRUE(property.GetStringOffsets().type.empty()); + ASSERT_TRUE(property.GetStringOffsets().data.data.empty()); + ASSERT_EQ(property.GetStringOffsets().data.target, 0); + } + + // Check property that describes names of the planet class. + { + const PropertyTable::Property &property = table.GetProperty(1); + ASSERT_EQ(property.GetName(), "name"); + const std::vector &data = property.GetData().data; + const std::vector &offsets = property.GetStringOffsets().data.data; + + ASSERT_EQ(data.size(), 296); // Concatenated label strings. + ASSERT_EQ(property.GetData().target, 34963); // ELEMENT_ARRAY_BUFFER. + + ASSERT_EQ(property.GetStringOffsets().type, "UINT32"); + ASSERT_EQ(offsets.size(), 4 * (kRows + 1)); + ASSERT_EQ(property.GetStringOffsets().data.target, 34963); + + ASSERT_EQ(offsets[0], 0); // Tatooine 0. + ASSERT_EQ(offsets[1], 0); + ASSERT_EQ(offsets[2], 0); + ASSERT_EQ(offsets[3], 0); + ASSERT_EQ(offsets[60], 32); // UNLABELED 287. + ASSERT_EQ(offsets[61], 1); + ASSERT_EQ(offsets[62], 0); + ASSERT_EQ(offsets[63], 0); + ASSERT_EQ(offsets[64], 41); // Beyond UNLABELED 296. + ASSERT_EQ(offsets[65], 1); + ASSERT_EQ(offsets[66], 0); + ASSERT_EQ(offsets[67], 0); + + struct Name { + static std::string Extract(const std::vector &data, + const std::vector &offsets, int row) { + const int b = offsets[4 * (row + 0)] + 255 * offsets[4 * (row + 0) + 1]; + const int e = offsets[4 * (row + 1)] + 255 * offsets[4 * (row + 1) + 1]; + return std::string(data.begin() + b, data.begin() + e); + } + }; + + // Check that the names can be extracted from the data. + ASSERT_EQ(Name::Extract(data, offsets, 0), "named_class:Tatooine"); + ASSERT_EQ(Name::Extract(data, offsets, 6), "named_class:Corellia"); + ASSERT_EQ(Name::Extract(data, offsets, 15), "UNLABELED"); + + ASSERT_TRUE(property.GetArrayOffsets().type.empty()); + ASSERT_TRUE(property.GetArrayOffsets().data.data.empty()); + ASSERT_EQ(property.GetArrayOffsets().data.target, 0); + } + + // Check property that describes number sequence of the planet class. + { + const PropertyTable::Property &property = table.GetProperty(2); + ASSERT_EQ(property.GetName(), "sequence"); + const std::vector &data = property.GetData().data; + const std::vector &offsets = property.GetArrayOffsets().data.data; + + ASSERT_EQ(data.size(), 41 * 4); // Concatenated float arrays. + ASSERT_EQ(property.GetData().target, 34963); // ELEMENT_ARRAY_BUFFER. + + ASSERT_EQ(property.GetArrayOffsets().type, "UINT8"); + ASSERT_EQ(offsets.size(), 20); // kRows + 1 + padding. + ASSERT_EQ(property.GetArrayOffsets().data.target, 34963); + + ASSERT_EQ(offsets[0], 0 * 4); // Tatooine + ASSERT_EQ(offsets[1], 6 * 4); // Corusant + ASSERT_EQ(offsets[6], 16 * 4); // Corellia + ASSERT_EQ(offsets[14], 36 * 4); // Geonosis + ASSERT_EQ(offsets[15], 41 * 4); // UNLABELED (empty array). + ASSERT_EQ(offsets[16], 41 * 4); // Beyond UNLABELED (empty array). + + struct Sequence { + static std::vector Extract(const std::vector &data, + const std::vector &offsets, + int row) { + const int n = (offsets[row + 1] - offsets[row]) / 4; + std::vector result; + result.reserve(n); + for (int i = 0; i < n; ++i) { + const void *const pointer = &data[offsets[row] + 4 * i]; + result.push_back(*static_cast(pointer)); + } + return result; + } + }; + + // Check that the number sequence arrays can be extracted from the data. + ASSERT_EQ( + Sequence::Extract(data, offsets, 0), + (std::vector{0.5f, 1.5f, 2.5f, 3.5f, 4.5f, 5.5f})); // Tatooine + ASSERT_EQ(Sequence::Extract(data, offsets, 1), + (std::vector{6.5f, 7.5f})); // Corusant + ASSERT_EQ( + Sequence::Extract(data, offsets, 14), + (std::vector{36.5f, 37.5f, 38.5f, 39.5f, 40.5f})); // Geonosis + ASSERT_TRUE(Sequence::Extract(data, offsets, 15) + .empty()); // UNLABELED (empty array). + + ASSERT_TRUE(property.GetStringOffsets().type.empty()); + ASSERT_TRUE(property.GetStringOffsets().data.data.empty()); + ASSERT_EQ(property.GetStringOffsets().data.target, 0); + } +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace draco diff --git a/contrib/draco/src/draco/io/gltf_test_helper.h b/contrib/draco/src/draco/io/gltf_test_helper.h new file mode 100644 index 000000000..91aec9b08 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_test_helper.h @@ -0,0 +1,61 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_GLTF_DECODER_TEST_HELPER_H_ +#define DRACO_IO_GLTF_DECODER_TEST_HELPER_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/scene/scene.h" + +namespace draco { + +// Helper class for testing Draco glTF encoder and decoder. +class GltfTestHelper { + public: + // Adds various mesh feature ID sets (via attributes and via textures) and + // structural metadata property table and property table schema to the box + // |scene| loaded from the test file testdata/Box/glTF/Box.gltf. + static void AddBoxMetaMeshFeatures(Scene *scene); + static void AddBoxMetaStructuralMetadata(Scene *scene); + + // Checks the box |geometry| (draco::Mesh or draco::Scene) with mesh features + // loaded from one of these test files, with or without Draco compression: + // 1. testdata/BoxMeta/glTF/BoxMeta.gltf + // 2. testdata/BoxMetaDraco/glTF/BoxMetaDraco.gltf + template + static void CheckBoxMetaMeshFeatures(const GeometryT &geometry, + bool has_draco_compression); + + // Checks the box |geometry| (draco::Mesh or draco::Scene) with structural + // metadata that includes property table and property table schema loaded from + // test file testdata/BoxMeta/glTF/BoxMeta.gltf. + template + static void CheckBoxMetaStructuralMetadata(const GeometryT &geometry) { + CheckBoxMetaStructuralMetadata(geometry.GetStructuralMetadata()); + } + + private: + static void CheckBoxMetaMeshFeatures(const Mesh &mesh, + const TextureLibrary &texture_lib, + bool has_draco_compression); + static void CheckBoxMetaStructuralMetadata( + const StructuralMetadata &structural_metadata); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_DECODER_TEST_HELPER_H_ diff --git a/contrib/draco/src/draco/io/gltf_utils.cc b/contrib/draco/src/draco/io/gltf_utils.cc new file mode 100644 index 000000000..bf5c048ef --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_utils.cc @@ -0,0 +1,154 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_utils.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +namespace draco { + +std::ostream &operator<<(std::ostream &os, const GltfValue &value) { + if (value.type_ == GltfValue::INT) { + os << value.value_int_; + } else { + os << value.value_double_; + } + return os; +} + +Indent::Indent() : indent_space_count_(2) {} + +void Indent::Increase() { indent_ += std::string(indent_space_count_, ' '); } + +void Indent::Decrease() { indent_.erase(0, indent_space_count_); } + +std::ostream &operator<<(std::ostream &os, const Indent &indent) { + return os << indent.indent_; +} + +std::ostream &operator<<(std::ostream &os, + const JsonWriter::IndentWrapper &indent) { + if (indent.writer.mode_ == JsonWriter::READABLE) { + os << indent.writer.indent_writer_; + } + return os; +} + +std::ostream &operator<<(std::ostream &os, + const JsonWriter::Separator &separator) { + if (separator.writer.mode_ == JsonWriter::READABLE) { + os << " "; + } + return os; +} + +void JsonWriter::Reset() { + last_type_ = START; + o_.clear(); + o_.str(""); +} + +void JsonWriter::BeginObject() { BeginObject(""); } + +void JsonWriter::BeginObject(const std::string &name) { + FinishPreviousLine(BEGIN); + o_ << indent_; + if (!name.empty()) { + o_ << "\"" << name << "\":" << separator_; + } + o_ << "{"; + indent_writer_.Increase(); +} + +void JsonWriter::EndObject() { + FinishPreviousLine(END); + indent_writer_.Decrease(); + o_ << indent_ << "}"; +} + +void JsonWriter::BeginArray(const std::string &name) { + FinishPreviousLine(BEGIN); + o_ << indent_ << "\"" << name << "\":" << separator_ << "["; + indent_writer_.Increase(); +} + +void JsonWriter::EndArray() { + FinishPreviousLine(END); + indent_writer_.Decrease(); + o_ << indent_ << "]"; +} + +void JsonWriter::FinishPreviousLine(OutputType curr_type) { + if (last_type_ != START) { + if ((last_type_ == VALUE && curr_type == VALUE) || + (last_type_ == VALUE && curr_type == BEGIN) || + (last_type_ == END && curr_type == BEGIN) || + (last_type_ == END && curr_type == VALUE)) { + o_ << ","; + } + if (mode_ == READABLE) { + o_ << std::endl; + } + } + last_type_ = curr_type; +} + +std::string JsonWriter::MoveData() { + const std::string str = o_.str(); + o_.str(""); + return str; +} + +std::string JsonWriter::EscapeCharacter(const std::string &str, + const char character) { + size_t start = 0; + if ((start = str.find(character, start)) != std::string::npos) { + std::string s = str; + std::string escaped_character = "\\"; + escaped_character += character; + do { + s.replace(start, 1, escaped_character); + start += escaped_character.length(); + } while ((start = s.find(character, start)) != std::string::npos); + return s; + } + return str; +} + +std::string JsonWriter::EscapeJsonSpecialCharacters(const std::string &str) { + std::string s = str; + const char backspace = '\b'; + const char form_feed = '\f'; + const char newline = '\n'; + const char carriage_return = '\r'; + const char tab = '\t'; + const char double_quote = '\"'; + const char backslash = '\\'; + + // Backslash must come first. + s = EscapeCharacter(s, backslash); + s = EscapeCharacter(s, backspace); + s = EscapeCharacter(s, form_feed); + s = EscapeCharacter(s, newline); + s = EscapeCharacter(s, carriage_return); + s = EscapeCharacter(s, tab); + s = EscapeCharacter(s, double_quote); + return s; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/gltf_utils.h b/contrib/draco/src/draco/io/gltf_utils.h new file mode 100644 index 000000000..2cf12fdc7 --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_utils.h @@ -0,0 +1,186 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_GLTF_UTILS_H_ +#define DRACO_IO_GLTF_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +namespace draco { + +// Class used to store integer or float values supported by glTF. +class GltfValue { + public: + enum ValueType { INT, DOUBLE }; + + explicit GltfValue(int8_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(uint8_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(int16_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(uint16_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(uint32_t value) + : type_(INT), value_int_(value), value_double_(-1.0) {} + + explicit GltfValue(float value) + : type_(DOUBLE), value_int_(-1), value_double_(value) {} + + friend std::ostream &operator<<(std::ostream &os, const GltfValue &value); + + private: + ValueType type_; + int64_t value_int_; + double value_double_; +}; + +// Utility class used to help with indentation of glTF file. +class Indent { + public: + Indent(); + + void Increase(); + void Decrease(); + + friend std::ostream &operator<<(std::ostream &os, const Indent &indent); + + private: + // Variables used for spacing of the glTF file. + std::string indent_; + const int indent_space_count_; +}; + +// Class used to keep track of the json state. +class JsonWriter { + public: + enum OutputType { START, BEGIN, END, VALUE }; + enum Mode { READABLE, COMPACT }; + + JsonWriter() + : last_type_(START), mode_(READABLE), indent_(*this), separator_(*this) {} + void SetMode(Mode mode) { mode_ = mode; } + + // Clear the stringstream and set last type to START. + void Reset(); + + // Every call to BeginObject should have a matching call to EndObject. + void BeginObject(); + void BeginObject(const std::string &name); + void EndObject(); + + // Every call to BeginArray should have a matching call to EndArray. + void BeginArray(const std::string &name); + void EndArray(); + + template + void OutputValue(const T &value) { + FinishPreviousLine(VALUE); + o_ << indent_ << std::setprecision(17) << value; + } + + void OutputValue(const bool &value) { + FinishPreviousLine(VALUE); + o_ << indent_ << ToString(value); + } + + void OutputValue(const std::string &name) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\""; + } + + void OutputValue(const std::string &name, const std::string &value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + const std::string escaped_value = EscapeJsonSpecialCharacters(value); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ << "\"" + << escaped_value << "\""; + } + + void OutputValue(const std::string &name, const char *value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + const std::string escaped_value = EscapeJsonSpecialCharacters(value); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ << "\"" + << escaped_value << "\""; + } + + template + void OutputValue(const std::string &name, const T &value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ << value; + } + + void OutputValue(const std::string &name, const bool &value) { + const std::string escaped_name = EscapeJsonSpecialCharacters(name); + FinishPreviousLine(VALUE); + o_ << indent_ << "\"" << escaped_name << "\":" << separator_ + << ToString(value); + } + + // Return the current output and then clear the stringstream. + std::string MoveData(); + + private: + // Check if a comma needs to be added to the output and then add a new line. + void FinishPreviousLine(OutputType curr_type); + + // Returns a string escaping all instances of |character| in |str|. + std::string EscapeCharacter(const std::string &str, const char character); + + // Returns a string escaping all of the Json special characters in |str|. + // Carriage return is not handled. + std::string EscapeJsonSpecialCharacters(const std::string &str); + + // Returns string representation of a Boolean |value|. + static std::string ToString(bool value) { return value ? "true" : "false"; } + + // Helper struct used for conditional indent writing to the output stream. + struct IndentWrapper { + explicit IndentWrapper(const JsonWriter &writer) : writer(writer) {} + const JsonWriter &writer; + }; + friend std::ostream &operator<<(std::ostream &os, + const IndentWrapper &indent); + + // Helper struct used for conditional separator writing to the output stream. + struct Separator { + explicit Separator(const JsonWriter &writer) : writer(writer) {} + const JsonWriter &writer; + }; + friend std::ostream &operator<<(std::ostream &os, const Separator &separator); + + std::stringstream o_; + Indent indent_writer_; + OutputType last_type_; + Mode mode_; + IndentWrapper indent_; + Separator separator_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_GLTF_UTILS_H_ diff --git a/contrib/draco/src/draco/io/gltf_utils_test.cc b/contrib/draco/src/draco/io/gltf_utils_test.cc new file mode 100644 index 000000000..01a2d144c --- /dev/null +++ b/contrib/draco/src/draco/io/gltf_utils_test.cc @@ -0,0 +1,366 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/gltf_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace draco { + +class GltfUtilsTest : public ::testing::Test { + protected: + void CompareGolden(JsonWriter *json_writer, const std::string &golden_str) { + const std::string json = json_writer->MoveData(); + ASSERT_EQ(golden_str, json); + } +}; + +TEST_F(GltfUtilsTest, TestNoData) { + const std::string golden = ""; + JsonWriter json_writer; + CompareGolden(&json_writer, golden); +} + +TEST_F(GltfUtilsTest, TestValues) { + JsonWriter json_writer; + json_writer.OutputValue(0); + CompareGolden(&json_writer, "0"); + + json_writer.Reset(); + json_writer.OutputValue(1); + CompareGolden(&json_writer, "1"); + + json_writer.Reset(); + json_writer.OutputValue(-1); + CompareGolden(&json_writer, "-1"); + + json_writer.Reset(); + json_writer.OutputValue(0.0); + CompareGolden(&json_writer, "0"); + + json_writer.Reset(); + json_writer.OutputValue(1.0); + CompareGolden(&json_writer, "1"); + + json_writer.Reset(); + json_writer.OutputValue(0.25); + CompareGolden(&json_writer, "0.25"); + + json_writer.Reset(); + json_writer.OutputValue(-0.25); + CompareGolden(&json_writer, "-0.25"); + + json_writer.Reset(); + json_writer.OutputValue(false); + CompareGolden(&json_writer, "false"); + + json_writer.Reset(); + json_writer.OutputValue(true); + CompareGolden(&json_writer, "true"); + + json_writer.Reset(); + json_writer.OutputValue("test int", -1); + CompareGolden(&json_writer, "\"test int\": -1"); + + json_writer.Reset(); + json_writer.OutputValue("test float", -10.25); + CompareGolden(&json_writer, "\"test float\": -10.25"); + + json_writer.Reset(); + json_writer.OutputValue("test char*", "I am the string!"); + CompareGolden(&json_writer, "\"test char*\": \"I am the string!\""); + + json_writer.Reset(); + const std::string value = "I am the string!"; + json_writer.OutputValue("test string", value); + CompareGolden(&json_writer, "\"test string\": \"I am the string!\""); + + json_writer.Reset(); + json_writer.OutputValue("test bool", false); + CompareGolden(&json_writer, "\"test bool\": false"); + + json_writer.Reset(); + json_writer.OutputValue("test bool", true); + CompareGolden(&json_writer, "\"test bool\": true"); +} + +TEST_F(GltfUtilsTest, TestSpecialCharacters) { + JsonWriter json_writer; + const std::string test_double_quote = "I am double quote\""; + json_writer.OutputValue("test double quote", test_double_quote); + CompareGolden(&json_writer, + "\"test double quote\": \"I am double quote\\\"\""); + + json_writer.Reset(); + const std::string test_backspace = "I am backspace\b"; + json_writer.OutputValue("test backspace", test_backspace); + CompareGolden(&json_writer, "\"test backspace\": \"I am backspace\\\b\""); + + json_writer.Reset(); + const std::string test_form_feed = "I am form feed\f"; + json_writer.OutputValue("test form feed", test_form_feed); + CompareGolden(&json_writer, "\"test form feed\": \"I am form feed\\\f\""); + + json_writer.Reset(); + const std::string test_newline = "I am newline\n"; + json_writer.OutputValue("test newline", test_newline); + CompareGolden(&json_writer, "\"test newline\": \"I am newline\\\n\""); + + json_writer.Reset(); + const std::string test_tab = "I am tab\t"; + json_writer.OutputValue("test tab", test_tab); + CompareGolden(&json_writer, "\"test tab\": \"I am tab\\\t\""); + + json_writer.Reset(); + const std::string test_backslash = "I am backslash\\"; + json_writer.OutputValue("test backslash", test_backslash); + CompareGolden(&json_writer, "\"test backslash\": \"I am backslash\\\\\""); + + json_writer.Reset(); + const std::string test_multiple_special_characters = "\"break\"and\\more\"\\"; + json_writer.OutputValue("test multiple_special_characters", + test_multiple_special_characters); + CompareGolden(&json_writer, + "\"test multiple_special_characters\": " + "\"\\\"break\\\"and\\\\more\\\"\\\\\""); +} + +TEST_F(GltfUtilsTest, TestObjects) { + JsonWriter json_writer; + json_writer.BeginObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "{\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\": {\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\": {\n 0\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\": {\n 0,\n 1,\n 2,\n 3\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.EndObject(); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\": {\n},\n\"object2\": {\n}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\": {\n \"object2\": {\n }\n}"); +} + +TEST_F(GltfUtilsTest, TestArrays) { + JsonWriter json_writer; + json_writer.BeginArray("array"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\": [\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\": [\n 0\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\": [\n 0,\n 1,\n 2,\n 3\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.EndArray(); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\": [\n],\n\"array2\": [\n]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\": [\n \"array2\": [\n ]\n]"); +} + +TEST_F(GltfUtilsTest, TestGltfValues) { + JsonWriter json_writer; + const int8_t int8_value_min = std::numeric_limits::min(); + const int8_t int8_value_max = std::numeric_limits::max(); + const GltfValue int8_value_low(int8_value_min); + const GltfValue int8_value_high(int8_value_max); + json_writer.OutputValue(int8_value_low); + json_writer.OutputValue(int8_value_high); + CompareGolden(&json_writer, "-128,\n127"); + + json_writer.Reset(); + const uint8_t uint8_value_min = std::numeric_limits::min(); + const uint8_t uint8_value_max = std::numeric_limits::max(); + const GltfValue uint8_value_low(uint8_value_min); + const GltfValue uint8_value_high(uint8_value_max); + json_writer.OutputValue(uint8_value_low); + json_writer.OutputValue(uint8_value_high); + CompareGolden(&json_writer, "0,\n255"); + + json_writer.Reset(); + const int16_t int16_value_min = std::numeric_limits::min(); + const int16_t int16_value_max = std::numeric_limits::max(); + const GltfValue int16_value_low(int16_value_min); + const GltfValue int16_value_high(int16_value_max); + json_writer.OutputValue(int16_value_low); + json_writer.OutputValue(int16_value_high); + CompareGolden(&json_writer, "-32768,\n32767"); + + json_writer.Reset(); + const uint16_t uint16_value_min = std::numeric_limits::min(); + const uint16_t uint16_value_max = std::numeric_limits::max(); + const GltfValue uint16_value_low(uint16_value_min); + const GltfValue uint16_value_high(uint16_value_max); + json_writer.OutputValue(uint16_value_low); + json_writer.OutputValue(uint16_value_high); + CompareGolden(&json_writer, "0,\n65535"); + + json_writer.Reset(); + const uint32_t uint32_value_min = std::numeric_limits::min(); + const uint32_t uint32_value_max = std::numeric_limits::max(); + const GltfValue uint32_value_low(uint32_value_min); + const GltfValue uint32_value_high(uint32_value_max); + json_writer.OutputValue(uint32_value_low); + json_writer.OutputValue(uint32_value_high); + CompareGolden(&json_writer, "0,\n4294967295"); + + json_writer.Reset(); + const float float_value_min = std::numeric_limits::min(); + const float float_value_max = std::numeric_limits::max(); + const GltfValue float_value_low(float_value_min); + const GltfValue float_value_high(float_value_max); + json_writer.OutputValue(float_value_low); + json_writer.OutputValue(float_value_high); + CompareGolden(&json_writer, + "1.1754943508222875e-38,\n3.4028234663852886e+38"); + + json_writer.Reset(); + const GltfValue float_value_0(0.1f); + const GltfValue float_value_1(1.f); + json_writer.OutputValue(float_value_0); + json_writer.OutputValue(float_value_1); + CompareGolden(&json_writer, "0.10000000149011612,\n1"); +} + +TEST_F(GltfUtilsTest, TestObjectsCompact) { + JsonWriter json_writer; + json_writer.SetMode(JsonWriter::COMPACT); + json_writer.BeginObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "{}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\":{}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\":{0}"); + + json_writer.Reset(); + json_writer.BeginObject("object"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object\":{0,1,2,3}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.EndObject(); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\":{},\"object2\":{}"); + + json_writer.Reset(); + json_writer.BeginObject("object1"); + json_writer.BeginObject("object2"); + json_writer.EndObject(); + json_writer.EndObject(); + CompareGolden(&json_writer, "\"object1\":{\"object2\":{}}"); +} + +TEST_F(GltfUtilsTest, TestArraysCompact) { + JsonWriter json_writer; + json_writer.SetMode(JsonWriter::COMPACT); + json_writer.BeginArray("array"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\":[]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\":[0]"); + + json_writer.Reset(); + json_writer.BeginArray("array"); + json_writer.OutputValue(0); + json_writer.OutputValue(1); + json_writer.OutputValue(2); + json_writer.OutputValue(3); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array\":[0,1,2,3]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.EndArray(); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\":[],\"array2\":[]"); + + json_writer.Reset(); + json_writer.BeginArray("array1"); + json_writer.BeginArray("array2"); + json_writer.EndArray(); + json_writer.EndArray(); + CompareGolden(&json_writer, "\"array1\":[\"array2\":[]]"); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/image_compression_options.h b/contrib/draco/src/draco/io/image_compression_options.h new file mode 100644 index 000000000..722bdbd64 --- /dev/null +++ b/contrib/draco/src/draco/io/image_compression_options.h @@ -0,0 +1,31 @@ +// Copyright 2020 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_IMAGE_COMPRESSION_OPTIONS_H_ +#define DRACO_IO_IMAGE_COMPRESSION_OPTIONS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +namespace draco { + +// Enum defining image compression formats. +enum class ImageFormat { NONE, PNG, JPEG, BASIS, WEBP }; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_IMAGE_COMPRESSION_OPTIONS_H_ diff --git a/contrib/draco/src/draco/io/mesh_io.cc b/contrib/draco/src/draco/io/mesh_io.cc index e0dc69c6f..4975d9236 100644 --- a/contrib/draco/src/draco/io/mesh_io.cc +++ b/contrib/draco/src/draco/io/mesh_io.cc @@ -18,8 +18,18 @@ #include #include "draco/io/file_utils.h" +#include "draco/io/file_writer_interface.h" #include "draco/io/obj_decoder.h" #include "draco/io/ply_decoder.h" +#include "draco/io/stl_decoder.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/compression/draco_compression_options.h" +#include "draco/compression/encode.h" +#include "draco/io/gltf_decoder.h" +#include "draco/io/gltf_encoder.h" +#include "draco/io/obj_encoder.h" +#include "draco/io/ply_encoder.h" +#endif namespace draco { @@ -46,27 +56,40 @@ StatusOr> ReadMeshFromFile( std::unique_ptr mesh(new Mesh()); // Analyze file extension. const std::string extension = LowercaseFileExtension(file_name); - if (extension != "gltf" && mesh_files) { - // The GLTF decoder will fill |mesh_files|, but for other file types we set - // the root file here to avoid duplicating code. + if (extension != "gltf" && extension != "obj" && mesh_files) { + // The GLTF/OBJ decoder will fill |mesh_files|, but for other file types we + // set the root file here to avoid duplicating code. mesh_files->push_back(file_name); } if (extension == "obj") { // Wavefront OBJ file format. ObjDecoder obj_decoder; obj_decoder.set_use_metadata(options.GetBool("use_metadata", false)); - const Status obj_status = obj_decoder.DecodeFromFile(file_name, mesh.get()); + obj_decoder.set_preserve_polygons(options.GetBool("preserve_polygons")); + const Status obj_status = + obj_decoder.DecodeFromFile(file_name, mesh.get(), mesh_files); if (!obj_status.ok()) { return obj_status; } return std::move(mesh); } if (extension == "ply") { - // Wavefront PLY file format. + // Stanford PLY file format. PlyDecoder ply_decoder; DRACO_RETURN_IF_ERROR(ply_decoder.DecodeFromFile(file_name, mesh.get())); return std::move(mesh); } + if (extension == "stl") { + // STL file format. + StlDecoder stl_decoder; + return stl_decoder.DecodeFromFile(file_name); + } +#ifdef DRACO_TRANSCODER_SUPPORTED + if (extension == "gltf" || extension == "glb") { + GltfDecoder gltf_decoder; + return gltf_decoder.DecodeFromFile(file_name, mesh_files); + } +#endif // Otherwise not an obj file. Assume the file was encoded with one of the // draco encoding methods. diff --git a/contrib/draco/src/draco/io/obj_decoder.cc b/contrib/draco/src/draco/io/obj_decoder.cc index 9b4eab626..c233c2b56 100644 --- a/contrib/draco/src/draco/io/obj_decoder.cc +++ b/contrib/draco/src/draco/io/obj_decoder.cc @@ -14,8 +14,10 @@ // #include "draco/io/obj_decoder.h" +#include #include #include +#include #include "draco/io/file_utils.h" #include "draco/io/parser_utils.h" @@ -36,15 +38,25 @@ ObjDecoder::ObjDecoder() norm_att_id_(-1), material_att_id_(-1), sub_obj_att_id_(-1), + added_edge_att_id_(-1), deduplicate_input_values_(true), last_material_id_(0), use_metadata_(false), + preserve_polygons_(false), + has_polygons_(false), + mesh_files_(nullptr), out_mesh_(nullptr), out_point_cloud_(nullptr) {} Status ObjDecoder::DecodeFromFile(const std::string &file_name, Mesh *out_mesh) { + return DecodeFromFile(file_name, out_mesh, nullptr); +} + +Status ObjDecoder::DecodeFromFile(const std::string &file_name, Mesh *out_mesh, + std::vector *mesh_files) { out_mesh_ = out_mesh; + mesh_files_ = mesh_files; return DecodeFromFile(file_name, static_cast(out_mesh)); } @@ -90,6 +102,10 @@ Status ObjDecoder::DecodeInternal() { return status; } + if (mesh_files_ && !input_file_name_.empty()) { + mesh_files_->push_back(input_file_name_); + } + bool use_identity_mapping = false; if (num_obj_faces_ == 0) { // Mesh has no faces. In this case we try to read the geometry as a point @@ -146,6 +162,24 @@ Status ObjDecoder::DecodeInternal() { norm_att_id_ = out_point_cloud_->AddAttribute(va, use_identity_mapping, num_normals_); } + if (preserve_polygons_ && has_polygons_) { + // Create attribute for polygon reconstruction. + GeometryAttribute va; + va.Init(GeometryAttribute::GENERIC, nullptr, 1, DT_UINT8, false, 1, 0); + PointCloud *const pc = out_point_cloud_; + added_edge_att_id_ = pc->AddAttribute(va, false, 2); + + // Set attribute values to zero and one representing old edge and new edge. + for (const uint8_t i : {0, 1}) { + const AttributeValueIndex avi(i); + pc->attribute(added_edge_att_id_)->SetAttributeValue(avi, &i); + } + + // Add attribute metadata with name. + std::unique_ptr metadata(new draco::AttributeMetadata()); + metadata->AddEntryString("name", "added_edges"); + pc->AddAttributeMetadata(added_edge_att_id_, std::move(metadata)); + } if (num_materials_ > 0 && num_obj_faces_ > 0) { GeometryAttribute va; const auto geometry_attribute_type = GeometryAttribute::GENERIC; @@ -381,6 +415,7 @@ bool ObjDecoder::ParseTexCoord(Status *status) { } bool ObjDecoder::ParseFace(Status *status) { + constexpr int kMaxCorners = 8; char c; if (!buffer()->Peek(&c)) { return false; @@ -391,37 +426,35 @@ bool ObjDecoder::ParseFace(Status *status) { // Face definition found! buffer()->Advance(1); if (!counting_mode_) { - std::array indices[4]; - // Parse face indices (we try to look for up to four to support quads). + std::array indices[kMaxCorners]; + // Parse face indices. int num_valid_indices = 0; - for (int i = 0; i < 4; ++i) { + for (int i = 0; i < kMaxCorners; ++i) { if (!ParseVertexIndices(&indices[i])) { - if (i == 3) { - break; // It's OK if there is no fourth vertex index. + if (i >= 3) { + break; // It's OK if there is no fourth or higher vertex index. } *status = Status(Status::DRACO_ERROR, "Failed to parse vertex indices"); return true; } ++num_valid_indices; } - // Process the first face. - for (int i = 0; i < 3; ++i) { - const PointIndex vert_id(3 * num_obj_faces_ + i); - MapPointToVertexIndices(vert_id, indices[i]); - } - ++num_obj_faces_; - if (num_valid_indices == 4) { - // Add an additional triangle for the quad. - // - // 3----2 - // | / | - // | / | - // 0----1 - // - const PointIndex vert_id(3 * num_obj_faces_); - MapPointToVertexIndices(vert_id, indices[0]); - MapPointToVertexIndices(vert_id + 1, indices[2]); - MapPointToVertexIndices(vert_id + 2, indices[3]); + // Split quads and other n-gons into n - 2 triangles. + const int nt = num_valid_indices - 2; + // Iterate over triangles. + for (int t = 0; t < nt; t++) { + // Iterate over corners. + for (int c = 0; c < 3; c++) { + const PointIndex vert_id(3 * num_obj_faces_ + c); + const int triangulated_index = Triangulate(t, c); + MapPointToVertexIndices(vert_id, indices[triangulated_index]); + // Save info about new edges that will allow us to reconstruct polygons. + if (added_edge_att_id_ >= 0) { + const AttributeValueIndex avi(IsNewEdge(nt, t, c)); + out_point_cloud_->attribute(added_edge_att_id_) + ->SetPointMapEntry(vert_id, avi); + } + } ++num_obj_faces_; } } else { @@ -443,12 +476,14 @@ bool ObjDecoder::ParseFace(Status *status) { } } } - if (num_indices < 3 || num_indices > 4) { - *status = - Status(Status::DRACO_ERROR, "Invalid number of indices on a face"); + if (num_indices > 3) { + has_polygons_ = true; + } + if (num_indices < 3 || num_indices > kMaxCorners) { + *status = ErrorStatus("Invalid number of indices on a face"); return false; } - // Either one or two new triangles. + // Either one or more new triangles. num_obj_faces_ += num_indices - 2; } parser::SkipLine(buffer()); @@ -478,6 +513,9 @@ bool ObjDecoder::ParseMaterialLib(Status *status) { parser::SkipLine(&line_buffer); if (!material_file_name_.empty()) { + if (mesh_files_) { + mesh_files_->push_back(material_file_name_); + } if (!ParseMaterialFile(material_file_name_, status)) { // Silently ignore problems with material files for now. return true; @@ -705,4 +743,44 @@ bool ObjDecoder::ParseMaterialFileDefinition(Status * /* status */) { return true; } +// Methods Triangulate() and IsNewEdge() are used for polygon triangulation and +// representation as an attribute for reconstruction in the decoder. +// +// Polygon reconstruction attribute is associated with every triangle corner and +// has values zero or one. Zero indicates that an edge opposite to the corner is +// present in the original mesh (dashed lines), and one indicates that the +// opposite edge has been added during polygon triangulation (dotted lines). +// +// Polygon triangulation is illustrated below. Pentagon ABCDE is split into +// three triangles ABC, ACD, ADE. It is sufficient to set polygon reconstruction +// attribute at corners ABC and ACD. The attribute at the second corner of all +// triangles except for the last is set to one. +// +// C D +// * --------- * +// /. 1 0 .| +// / . . | +// / . . | +// / 0 . . 0 | +// / . . | +// B * 1 . . | +// \ . . | +// \ 0 . 0 . | +// \ . . | +// \ . . | +// \.. 0 0 | +// *-----------* +// A E +// +inline int ObjDecoder::Triangulate(int tri_index, int tri_corner) { + return tri_corner == 0 ? 0 : tri_index + tri_corner; +} + +inline bool ObjDecoder::IsNewEdge(int tri_count, int tri_index, + int tri_corner) { + // All but the last triangle of the triangulated polygon have an added edge + // opposite of corner 1. + return tri_index != tri_count - 1 && tri_corner == 1; +} + } // namespace draco diff --git a/contrib/draco/src/draco/io/obj_decoder.h b/contrib/draco/src/draco/io/obj_decoder.h index baeab5b0c..18dc9aadd 100644 --- a/contrib/draco/src/draco/io/obj_decoder.h +++ b/contrib/draco/src/draco/io/obj_decoder.h @@ -34,8 +34,12 @@ class ObjDecoder { ObjDecoder(); // Decodes an obj file stored in the input file. - // Returns nullptr if the decoding failed. + // Optional argument |mesh_files| will be populated with all paths to files + // relevant to the loaded mesh. Status DecodeFromFile(const std::string &file_name, Mesh *out_mesh); + Status DecodeFromFile(const std::string &file_name, Mesh *out_mesh, + std::vector *mesh_files); + Status DecodeFromFile(const std::string &file_name, PointCloud *out_point_cloud); @@ -50,6 +54,8 @@ class ObjDecoder { // Flag for whether using metadata to record other information in the obj // file, e.g. material names, object names. void set_use_metadata(bool flag) { use_metadata_ = flag; } + // Enables preservation of polygons. + void set_preserve_polygons(bool flag) { preserve_polygons_ = flag; } protected: Status DecodeInternal(); @@ -88,6 +94,11 @@ class ObjDecoder { bool ParseMaterialFile(const std::string &file_name, Status *status); bool ParseMaterialFileDefinition(Status *status); + // Methods related to polygon triangulation and preservation. + static int Triangulate(int tri_index, int tri_corner); + static bool IsNewEdge(int tri_count, int tri_index, int tri_corner); + + private: // If set to true, the parser will count the number of various definitions // but it will not parse the actual data or add any new entries to the mesh. bool counting_mode_; @@ -102,7 +113,8 @@ class ObjDecoder { int tex_att_id_; int norm_att_id_; int material_att_id_; - int sub_obj_att_id_; // Attribute id for storing sub-objects. + int sub_obj_att_id_; // Attribute id for storing sub-objects. + int added_edge_att_id_; // Attribute id for polygon reconstruction. bool deduplicate_input_values_; @@ -116,6 +128,12 @@ class ObjDecoder { bool use_metadata_; + // Polygon preservation flags. + bool preserve_polygons_; + bool has_polygons_; + + std::vector *mesh_files_; + DecoderBuffer buffer_; // Data structure that stores the decoded data. |out_point_cloud_| must be diff --git a/contrib/draco/src/draco/io/obj_decoder_test.cc b/contrib/draco/src/draco/io/obj_decoder_test.cc index b19fe6e2c..a46a15a8b 100644 --- a/contrib/draco/src/draco/io/obj_decoder_test.cc +++ b/contrib/draco/src/draco/io/obj_decoder_test.cc @@ -54,6 +54,20 @@ class ObjDecoderTest : public ::testing::Test { return geometry; } + template + std::unique_ptr DecodeObjWithPolygons( + const std::string &file_name, bool regularize_quads, + bool store_added_edges_per_vertex) const { + const std::string path = GetTestFileFullPath(file_name); + ObjDecoder decoder; + decoder.set_preserve_polygons(true); + std::unique_ptr geometry(new Geometry()); + if (!decoder.DecodeFromFile(path, geometry.get()).ok()) { + return nullptr; + } + return geometry; + } + void test_decoding(const std::string &file_name) { const std::unique_ptr mesh(DecodeObj(file_name)); ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; @@ -113,7 +127,7 @@ TEST_F(ObjDecoderTest, SubObjectsWithMetadata) { ASSERT_EQ(sub_obj_id, 2); } -TEST_F(ObjDecoderTest, QuadOBJ) { +TEST_F(ObjDecoderTest, QuadTriangulateOBJ) { // Tests loading an Obj with quad faces. const std::string file_name = "cube_quads.obj"; const std::unique_ptr mesh(DecodeObj(file_name)); @@ -124,11 +138,114 @@ TEST_F(ObjDecoderTest, QuadOBJ) { ASSERT_EQ(mesh->num_points(), 4 * 6); // Four points per quad face. } -TEST_F(ObjDecoderTest, ComplexPolyOBJ) { - // Tests that we fail to load an obj with complex polygon (expected failure). - const std::string file_name = "invalid/complex_poly.obj"; +TEST_F(ObjDecoderTest, QuadPreserveOBJ) { + // Tests loading an Obj with quad faces preserved as an attribute. + const std::string file_name = "cube_quads.obj"; + constexpr bool kRegularizeQuads = false; + constexpr bool kStoreAddedEdgesPerVertex = false; + const std::unique_ptr mesh(DecodeObjWithPolygons( + file_name, kRegularizeQuads, kStoreAddedEdgesPerVertex)); + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + ASSERT_EQ(mesh->num_faces(), 12); + + ASSERT_EQ(mesh->num_attributes(), 4); + ASSERT_EQ(mesh->num_points(), 4 * 6); // Four points per quad face. + + // Expect a new generic attribute. + ASSERT_EQ(mesh->attribute(3)->attribute_type(), GeometryAttribute::GENERIC); + + // Expect the new attribute to have two values to describe old and new edge. + ASSERT_EQ(mesh->attribute(3)->size(), 2); + const auto new_edge_value = + mesh->attribute(3)->GetValue(AttributeValueIndex(0))[0]; + const auto old_edge_value = + mesh->attribute(3)->GetValue(AttributeValueIndex(1))[0]; + ASSERT_EQ(new_edge_value, 0); + ASSERT_EQ(old_edge_value, 1); + + // Expect one new edge on each of the six cube quads. + for (int i = 0; i < 6; i++) { + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 0)), 0); + // New edge. + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 1)), 1); + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 2)), 0); + ASSERT_EQ(mesh->attribute(3)->mapped_index(PointIndex(4 * i + 3)), 0); + } + + // Expect metadata entry on the new attribute. + const AttributeMetadata *const metadata = + mesh->GetAttributeMetadataByAttributeId(3); + ASSERT_NE(metadata, nullptr); + ASSERT_TRUE(metadata->sub_metadatas().empty()); + ASSERT_EQ(metadata->entries().size(), 1); + std::string name; + metadata->GetEntryString("name", &name); + ASSERT_EQ(name, "added_edges"); +} + +TEST_F(ObjDecoderTest, OctagonTriangulatedOBJ) { + // Tests that we can load an obj with an octagon triangulated. + const std::string file_name = "octagon.obj"; const std::unique_ptr mesh(DecodeObj(file_name)); - ASSERT_EQ(mesh, nullptr); + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + + ASSERT_EQ(mesh->num_attributes(), 1); + ASSERT_EQ(mesh->num_points(), 8); + ASSERT_EQ(mesh->attribute(0)->attribute_type(), GeometryAttribute::POSITION); + ASSERT_EQ(mesh->attribute(0)->size(), 8); +} + +TEST_F(ObjDecoderTest, OctagonPreservedOBJ) { + // Tests that we can load an obj with an octagon preserved as an attribute. + const std::string file_name = "octagon.obj"; + constexpr bool kRegularizeQuads = false; + constexpr bool kStoreAddedEdgesPerVertex = false; + const std::unique_ptr mesh(DecodeObjWithPolygons( + file_name, kRegularizeQuads, kStoreAddedEdgesPerVertex)); + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + + ASSERT_EQ(mesh->num_attributes(), 2); + ASSERT_EQ(mesh->attribute(0)->attribute_type(), GeometryAttribute::POSITION); + ASSERT_EQ(mesh->attribute(0)->size(), 8); + + // Expect a new generic attribute. + ASSERT_EQ(mesh->attribute(1)->attribute_type(), GeometryAttribute::GENERIC); + + // There are four vertices with both old and new edges in their ring. + ASSERT_EQ(mesh->num_points(), 8 + 4); + + // Expect the new attribute to have two values to describe old and new edge. + ASSERT_EQ(mesh->attribute(1)->size(), 2); + const auto new_edge_value = + mesh->attribute(1)->GetValue(AttributeValueIndex(0))[0]; + const auto old_edge_value = + mesh->attribute(1)->GetValue(AttributeValueIndex(1))[0]; + ASSERT_EQ(new_edge_value, 0); + ASSERT_EQ(old_edge_value, 1); + + // Five new edges are introduced while triangulating as octagon. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(0)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(1)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(2)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(3)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(4)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(5)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(6)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(7)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(8)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(9)), 1); // New edge. + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(10)), 0); + ASSERT_EQ(mesh->attribute(1)->mapped_index(PointIndex(11)), 0); + + // Expect metadata entry on the new attribute. + const AttributeMetadata *const metadata = + mesh->GetAttributeMetadataByAttributeId(1); + ASSERT_NE(metadata, nullptr); + ASSERT_TRUE(metadata->sub_metadatas().empty()); + ASSERT_EQ(metadata->entries().size(), 1); + std::string name; + metadata->GetEntryString("name", &name); + ASSERT_EQ(name, "added_edges"); } TEST_F(ObjDecoderTest, EmptyNameOBJ) { @@ -167,7 +284,6 @@ TEST_F(ObjDecoderTest, WrongAttributeMapping) { TEST_F(ObjDecoderTest, TestObjDecodingAll) { // test if we can read all obj that are currently in test folder. test_decoding("bunny_norm.obj"); - // test_decoding("complex_poly.obj"); // not supported see test above test_decoding("cube_att.obj"); test_decoding("cube_att_partial.obj"); test_decoding("cube_att_sub_o.obj"); diff --git a/contrib/draco/src/draco/io/obj_encoder.cc b/contrib/draco/src/draco/io/obj_encoder.cc index 29c6ca8f0..1ddfd92bd 100644 --- a/contrib/draco/src/draco/io/obj_encoder.cc +++ b/contrib/draco/src/draco/io/obj_encoder.cc @@ -16,8 +16,10 @@ #include +#include "draco/attributes/geometry_attribute.h" #include "draco/io/file_writer_factory.h" #include "draco/io/file_writer_interface.h" +#include "draco/mesh/mesh_misc_functions.h" #include "draco/metadata/geometry_metadata.h" namespace draco { @@ -28,6 +30,7 @@ ObjEncoder::ObjEncoder() normal_att_(nullptr), material_att_(nullptr), sub_obj_att_(nullptr), + added_edges_att_(nullptr), out_buffer_(nullptr), in_point_cloud_(nullptr), in_mesh_(nullptr), @@ -78,11 +81,15 @@ bool ObjEncoder::EncodeInternal() { normal_att_ = nullptr; material_att_ = nullptr; sub_obj_att_ = nullptr; + added_edges_att_ = nullptr; current_sub_obj_id_ = -1; current_material_id_ = -1; if (!GetSubObjects()) { return false; } + if (in_mesh_ && !GetAddedEdges()) { + return false; + } if (!EncodeMaterialFileName()) { return false; } @@ -110,12 +117,38 @@ bool ObjEncoder::ExitAndCleanup(bool return_value) { normal_att_ = nullptr; material_att_ = nullptr; sub_obj_att_ = nullptr; + added_edges_att_ = nullptr; current_sub_obj_id_ = -1; current_material_id_ = -1; file_name_.clear(); return return_value; } +bool ObjEncoder::GetAddedEdges() { + const GeometryMetadata *mesh_metadata = in_mesh_->GetMetadata(); + if (!mesh_metadata) { + return true; + } + + // Try to get a per-corner attribute describing added edges. + { + const AttributeMetadata *att_metadata = + mesh_metadata->GetAttributeMetadataByStringEntry("name", "added_edges"); + if (att_metadata) { + const auto att = + in_mesh_->GetAttributeByUniqueId(att_metadata->att_unique_id()); + if (att->size() == 0 || att->num_components() != 1 || + att->data_type() != DataType::DT_UINT8) { + return false; + } + added_edges_att_ = att; + return true; + } + } + + return true; +} + bool ObjEncoder::GetSubObjects() { const GeometryMetadata *pc_metadata = in_point_cloud_->GetMetadata(); if (!pc_metadata) { @@ -137,7 +170,8 @@ bool ObjEncoder::GetSubObjects() { } sub_obj_att_ = in_point_cloud_->GetAttributeByUniqueId( sub_obj_metadata->att_unique_id()); - if (sub_obj_att_ == nullptr || sub_obj_att_->size() == 0) { + if (sub_obj_att_ == nullptr || sub_obj_att_->size() == 0 || + sub_obj_att_->num_components() != 1) { return false; } return true; @@ -236,17 +270,11 @@ bool ObjEncoder::EncodeNormals() { } bool ObjEncoder::EncodeFaces() { + if (added_edges_att_ != nullptr) { + return EncodePolygonalFaces(); + } for (FaceIndex i(0); i < in_mesh_->num_faces(); ++i) { - if (sub_obj_att_) { - if (!EncodeSubObject(i)) { - return false; - } - } - if (material_att_) { - if (!EncodeMaterial(i)) { - return false; - } - } + EncodeFaceAttributes(i); buffer()->Encode('f'); for (int j = 0; j < 3; ++j) { if (!EncodeFaceCorner(i, j)) { @@ -258,6 +286,56 @@ bool ObjEncoder::EncodeFaces() { return true; } +bool ObjEncoder::EncodePolygonalFaces() { + // TODO(vytyaz): This could be a much smaller set of visited face indices. + std::vector triangle_visited(in_mesh_->num_faces(), false); + PolygonEdges polygon_edges; + std::unique_ptr corner_table = + CreateCornerTableFromPositionAttribute(in_mesh_); + for (FaceIndex fi(0); fi < in_mesh_->num_faces(); ++fi) { + EncodeFaceAttributes(fi); + // Reconstruct polygon from the added edges attribute if available. + polygon_edges.clear(); + FindOriginalFaceEdges(fi, *corner_table, &triangle_visited, &polygon_edges); + + // Polygon edges could be empty if this triangle has been visited as part + // of a polygon discovery that started from an earler face. + if (polygon_edges.empty()) { + continue; + } + + // Traverse a polygon by following its edges. The starting point is not + // guaranteed to be the same as in the original polygon. It is + // deterministic, however, and defined by std::map behavior. + const AttributeValueIndex first_position_index = + polygon_edges.begin()->first; + AttributeValueIndex position_index = first_position_index; + buffer()->Encode('f'); + do { + // Get the next polygon point index by following polygon edge. + const PointIndex pi = polygon_edges[position_index]; + EncodeFaceCorner(pi); + position_index = pos_att_->mapped_index(pi).value(); + } while (position_index != first_position_index); + buffer()->Encode("\n", 1); + } + return true; +} + +bool ObjEncoder::EncodeFaceAttributes(FaceIndex face_id) { + if (sub_obj_att_) { + if (!EncodeSubObject(face_id)) { + return false; + } + } + if (material_att_) { + if (!EncodeMaterial(face_id)) { + return false; + } + } + return true; +} + bool ObjEncoder::EncodeMaterial(FaceIndex face_id) { int material_id = 0; // Pick the first corner, all corners of a face should have same id. @@ -304,8 +382,12 @@ bool ObjEncoder::EncodeSubObject(FaceIndex face_id) { } bool ObjEncoder::EncodeFaceCorner(FaceIndex face_id, int local_corner_id) { - buffer()->Encode(' '); const PointIndex vert_index = in_mesh_->face(face_id)[local_corner_id]; + return EncodeFaceCorner(vert_index); +} + +bool ObjEncoder::EncodeFaceCorner(PointIndex vert_index) { + buffer()->Encode(' '); // Note that in the OBJ format, all indices are encoded starting from index 1. // Encode position index. EncodeInt(pos_att_->mapped_index(vert_index).value() + 1); @@ -343,4 +425,67 @@ void ObjEncoder::EncodeInt(int32_t val) { buffer()->Encode(num_buffer_, strlen(num_buffer_)); } +bool ObjEncoder::IsNewEdge(const CornerTable &ct, CornerIndex ci) const { + const PointIndex pi = in_mesh_->CornerToPointId(ci); + if (added_edges_att_ != nullptr) { + uint8_t value; + added_edges_att_->GetMappedValue(pi, &value); + return value == 1; + } + return false; +} + +void ObjEncoder::FindOriginalFaceEdges(FaceIndex face_index, + const CornerTable &corner_table, + std::vector *triangle_visited, + PolygonEdges *polygon_edges) { + // Do not add any edges if this triangular face has already been visited. + if ((*triangle_visited)[face_index.value()]) { + return; + } + (*triangle_visited)[face_index.value()] = true; + const Mesh::Face &face = in_mesh_->face(face_index); + for (size_t c = 0; c < 3; c++) { + // Check for added edge using this corner. + const CornerIndex ci = corner_table.FirstCorner(face_index) + c; + const CornerIndex co = corner_table.Opposite(ci); + bool is_new_edge = IsNewEdge(corner_table, ci); + + // Check for the new edge using the opposite corner. + if (!is_new_edge && co != kInvalidCornerIndex) { + is_new_edge = IsNewEdge(corner_table, co); + } + // The new edge may become a boundary edge when a degenerate triangle + // created by polygon triangulation is removed by Draco encoder, hence |co| + // is checked below. This can happen when an isolated (boundary) quad only + // has three distinct vertex positions. + // + // TODO(vytyaz): Fix polygon reconstruction with other possible cases of + // degenerate triangles. There are two known sources of degenerate triangles + // that affect polygon reconstruction: + // + // 1. Degenerate triangles created during polygon triangulation are removed + // by Draco encoder, which invalidates the "added_edges" attribute. + // Solution is to discard those triangles before creating the attribute. + // + // 2. Degenerate triangles created by position quantization are encoded and + // decoded by Draco, but not captured into the |corner_table|, causing a + // mismatch between the corner table and the "added_edges" attribute. + // Solution is to use corner table from draco::MeshDecoder here. + // + if (is_new_edge && co != kInvalidCornerIndex) { + // Visit triangle across the new edge. + const FaceIndex opposite_face_index = corner_table.Face(co); + FindOriginalFaceEdges(opposite_face_index, corner_table, triangle_visited, + polygon_edges); + } else { + // Insert the original edge to the map. + const PointIndex point_from = face[(c + 1) % 3]; + const PointIndex point_to = face[(c + 2) % 3]; + polygon_edges->insert( + {PositionIndex(pos_att_->mapped_index(point_from)), point_to}); + } + } +} + } // namespace draco diff --git a/contrib/draco/src/draco/io/obj_encoder.h b/contrib/draco/src/draco/io/obj_encoder.h index 509d39baf..1d67b5306 100644 --- a/contrib/draco/src/draco/io/obj_encoder.h +++ b/contrib/draco/src/draco/io/obj_encoder.h @@ -18,6 +18,7 @@ #include #include "draco/core/encoder_buffer.h" +#include "draco/mesh/corner_table.h" #include "draco/mesh/mesh.h" namespace draco { @@ -44,19 +45,30 @@ class ObjEncoder { bool ExitAndCleanup(bool return_value); private: + typedef AttributeValueIndex PositionIndex; + typedef std::map PolygonEdges; + bool GetAddedEdges(); bool GetSubObjects(); bool EncodeMaterialFileName(); bool EncodePositions(); bool EncodeTextureCoordinates(); bool EncodeNormals(); bool EncodeFaces(); + bool EncodePolygonalFaces(); + bool EncodeFaceAttributes(FaceIndex face_id); bool EncodeSubObject(FaceIndex face_id); bool EncodeMaterial(FaceIndex face_id); bool EncodeFaceCorner(FaceIndex face_id, int local_corner_id); + bool EncodeFaceCorner(PointIndex vert_index); void EncodeFloat(float val); void EncodeFloatList(float *vals, int num_vals); void EncodeInt(int32_t val); + bool IsNewEdge(const CornerTable &ct, CornerIndex ci) const; + void FindOriginalFaceEdges(FaceIndex face_index, + const CornerTable &corner_table, + std::vector *triangle_visited, + PolygonEdges *polygon_edges); // Various attributes used by the encoder. If an attribute is not used, it is // set to nullptr. @@ -66,6 +78,9 @@ class ObjEncoder { const PointAttribute *material_att_; const PointAttribute *sub_obj_att_; + // Stores per-corner triangulation information for polygon reconstruction. + const PointAttribute *added_edges_att_; + // Buffer used for encoding float/int numbers. char num_buffer_[20]; diff --git a/contrib/draco/src/draco/io/obj_encoder_test.cc b/contrib/draco/src/draco/io/obj_encoder_test.cc index 4838e56ca..782983fad 100644 --- a/contrib/draco/src/draco/io/obj_encoder_test.cc +++ b/contrib/draco/src/draco/io/obj_encoder_test.cc @@ -16,10 +16,12 @@ #include +#include "draco/attributes/geometry_attribute.h" #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/io/file_reader_factory.h" #include "draco/io/file_reader_interface.h" +#include "draco/io/file_utils.h" #include "draco/io/obj_decoder.h" namespace draco { @@ -27,6 +29,8 @@ namespace draco { class ObjEncoderTest : public ::testing::Test { protected: void CompareMeshes(const Mesh *mesh0, const Mesh *mesh1) { + ASSERT_NE(mesh0, nullptr); + ASSERT_NE(mesh1, nullptr); ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces()); ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes()); for (size_t att_id = 0; att_id < mesh0->num_attributes(); ++att_id) { @@ -107,4 +111,34 @@ TEST_F(ObjEncoderTest, TestObjEncodingAll) { test_encoding("two_faces_312.obj"); } +TEST_F(ObjEncoderTest, TestObjOctagonPreserved) { + // Test verifies that OBJ encoder can reconstruct and encode an octagon. + // Decode triangulated octagon and an extra attribute for reconstruction. + std::unique_ptr mesh = + ReadMeshFromTestFile("octagon_preserved.drc"); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_faces(), 6); + ASSERT_EQ(mesh->NumNamedAttributes(GeometryAttribute::GENERIC), 1); + ASSERT_NE(mesh->GetMetadata()->GetAttributeMetadataByStringEntry( + "name", "added_edges"), + nullptr); + + // Reconstruct octagon and encode it into an OBJ file. + draco::ObjEncoder obj_encoder; + ASSERT_TRUE(obj_encoder.EncodeToFile( + *mesh, draco::GetTestTempFileFullPath("encoded.obj"))); + + // Read encoded OBJ file and golden OBJ file contents into buffers. + std::vector data_encoded; + std::vector data_golden; + ASSERT_TRUE( + ReadFileToBuffer(GetTestTempFileFullPath("encoded.obj"), &data_encoded)); + ASSERT_TRUE(ReadFileToBuffer(GetTestFileFullPath("octagon_preserved.obj"), + &data_golden)); + + // Check that encoded OBJ file contents are correct. + ASSERT_EQ(data_encoded.size(), data_golden.size()); + ASSERT_EQ(data_encoded, data_golden); +} + } // namespace draco diff --git a/contrib/draco/src/draco/io/parser_utils.cc b/contrib/draco/src/draco/io/parser_utils.cc index 12afacff6..378de7378 100644 --- a/contrib/draco/src/draco/io/parser_utils.cc +++ b/contrib/draco/src/draco/io/parser_utils.cc @@ -203,31 +203,40 @@ void ParseLine(DecoderBuffer *buffer, std::string *out_string) { out_string->clear(); } char c; - bool delim_reached = false; + int num_delims = 0; + char last_delim; while (buffer->Peek(&c)) { - // Check if |c| is a delimeter. We want to parse all delimeters until we - // reach a non-delimeter symbol. (E.g. we want to ignore '\r\n' at the end - // of the line). + // Check if |c| is a delimiter symbol. We want to identify all possible + // delimiters that can occur on different platforms (i.e. we want to detect + // '\r\n', '\r', '\n'). const bool is_delim = (c == '\r' || c == '\n'); - // If |c| is a delimeter or it is a non-delimeter symbol before any - // delimeter was found, we advance the buffer to the next character. - if (is_delim || !delim_reached) { - buffer->Advance(1); + if (is_delim) { + if (num_delims == 0) { + last_delim = c; + } else if (num_delims == 1) { + // We already parsed either '\r' or '\n'. Ensure the new delim symbol is + // '\n' and different from the previous symbol. + if (c == last_delim || c != '\n') { + return; // Same delimiter symbol already processed. + } + } else { + // Too many delimiter symbols. + return; + } + num_delims++; } - if (is_delim) { - // Mark that we found a delimeter symbol. - delim_reached = true; - continue; - } - if (delim_reached) { - // We reached a non-delimeter symbol after a delimeter was already found. + if (!is_delim && num_delims > 0) { + // We reached a non-delimiter symbol after a delimiter was already found. // Stop the parsing. return; } - // Otherwise we put the non-delimeter symbol into the output string. - if (out_string) { + + buffer->Advance(1); + + // We put the non-delimiter symbol into the output string. + if (!is_delim && out_string) { out_string->push_back(c); } } diff --git a/contrib/draco/src/draco/io/ply_decoder_test.cc b/contrib/draco/src/draco/io/ply_decoder_test.cc index 97977c8cc..1dd70d5cb 100644 --- a/contrib/draco/src/draco/io/ply_decoder_test.cc +++ b/contrib/draco/src/draco/io/ply_decoder_test.cc @@ -88,6 +88,7 @@ TEST_F(PlyDecoderTest, TestPlyDecodingAll) { // test_decoding("test_pos_color.ply"); // tested test_decoding("cube_quads.ply"); test_decoding("Box.ply"); + test_decoding("delim_test.ply"); } } // namespace draco diff --git a/contrib/draco/src/draco/io/ply_encoder.cc b/contrib/draco/src/draco/io/ply_encoder.cc index 2f6a1a2a8..0fe611f1c 100644 --- a/contrib/draco/src/draco/io/ply_encoder.cc +++ b/contrib/draco/src/draco/io/ply_encoder.cc @@ -143,7 +143,8 @@ bool PlyEncoder::EncodeInternal() { buffer()->Encode(header_str.data(), header_str.length()); // Store point attributes. - for (PointIndex v(0); v < in_point_cloud_->num_points(); ++v) { + const int num_points = in_point_cloud_->num_points(); + for (PointIndex v(0); v < num_points; ++v) { const auto *const pos_att = in_point_cloud_->attribute(pos_att_id); buffer()->Encode(pos_att->GetAddress(pos_att->mapped_index(v)), pos_att->byte_stride()); @@ -166,9 +167,13 @@ bool PlyEncoder::EncodeInternal() { buffer()->Encode(static_cast(3)); const auto &f = in_mesh_->face(i); - buffer()->Encode(f[0]); - buffer()->Encode(f[1]); - buffer()->Encode(f[2]); + for (int c = 0; c < 3; ++c) { + if (f[c] >= num_points) { + // Invalid point stored on the |in_mesh_| face. + return false; + } + buffer()->Encode(f[c]); + } if (tex_coord_att_id >= 0) { // Two coordinates for every corner -> 6. diff --git a/contrib/draco/src/draco/io/ply_reader_test.cc b/contrib/draco/src/draco/io/ply_reader_test.cc index 05ff63dd4..9612f6377 100644 --- a/contrib/draco/src/draco/io/ply_reader_test.cc +++ b/contrib/draco/src/draco/io/ply_reader_test.cc @@ -39,7 +39,7 @@ TEST_F(PlyReaderTest, TestReader) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), 2); ASSERT_EQ(reader.element(0).num_properties(), 7); ASSERT_EQ(reader.element(1).num_properties(), 1); @@ -64,14 +64,14 @@ TEST_F(PlyReaderTest, TestReaderAscii) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); const std::string file_name_ascii = "test_pos_color_ascii.ply"; const std::vector data_ascii = ReadPlyFile(file_name_ascii); buf.Init(data_ascii.data(), data_ascii.size()); PlyReader reader_ascii; status = reader_ascii.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), reader_ascii.num_elements()); ASSERT_EQ(reader.element(0).num_properties(), reader_ascii.element(0).num_properties()); @@ -96,7 +96,7 @@ TEST_F(PlyReaderTest, TestReaderExtraWhitespace) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), 2); ASSERT_EQ(reader.element(0).num_properties(), 7); @@ -122,7 +122,7 @@ TEST_F(PlyReaderTest, TestReaderMoreDataTypes) { buf.Init(data.data(), data.size()); PlyReader reader; Status status = reader.Read(&buf); - ASSERT_TRUE(status.ok()) << status; + DRACO_ASSERT_OK(status); ASSERT_EQ(reader.num_elements(), 2); ASSERT_EQ(reader.element(0).num_properties(), 7); diff --git a/contrib/draco/src/draco/io/scene_io.cc b/contrib/draco/src/draco/io/scene_io.cc new file mode 100644 index 000000000..e41d2e1fa --- /dev/null +++ b/contrib/draco/src/draco/io/scene_io.cc @@ -0,0 +1,127 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/scene_io.h" + +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/io/file_utils.h" +#include "draco/io/gltf_decoder.h" +#include "draco/io/gltf_encoder.h" +#include "draco/io/obj_encoder.h" +#include "draco/io/ply_encoder.h" + +namespace draco { + +enum SceneFileFormat { UNKNOWN, GLTF, USD, PLY, OBJ }; + +SceneFileFormat GetSceneFileFormat(const std::string &file_name) { + const std::string extension = LowercaseFileExtension(file_name); + if (extension == "gltf" || extension == "glb") { + return GLTF; + } + if (extension == "usd" || extension == "usda" || extension == "usdc" || + extension == "usdz") { + return USD; + } + if (extension == "obj") { + return OBJ; + } + if (extension == "ply") { + return PLY; + } + return UNKNOWN; +} + +StatusOr> ReadSceneFromFile( + const std::string &file_name) { + return ReadSceneFromFile(file_name, nullptr); +} + +StatusOr> ReadSceneFromFile( + const std::string &file_name, std::vector *scene_files) { + std::unique_ptr scene(new Scene()); + switch (GetSceneFileFormat(file_name)) { + case GLTF: { + GltfDecoder decoder; + return decoder.DecodeFromFileToScene(file_name, scene_files); + } + case USD: { + return Status(Status::DRACO_ERROR, "USD is not supported yet."); + } + default: { + return Status(Status::DRACO_ERROR, "Unknown input file format."); + } + } +} + +Status WriteSceneToFile(const std::string &file_name, const Scene &scene) { + Options options; + return WriteSceneToFile(file_name, scene, options); +} + +Status WriteSceneToFile(const std::string &file_name, const Scene &scene, + const Options &options) { + const std::string extension = LowercaseFileExtension(file_name); + std::string folder_path; + std::string out_file_name; + draco::SplitPath(file_name, &folder_path, &out_file_name); + const auto format = GetSceneFileFormat(file_name); + switch (format) { + case GLTF: { + GltfEncoder encoder; + if (!encoder.EncodeToFile(scene, file_name, folder_path)) { + return Status(Status::DRACO_ERROR, "Failed to encode the scene."); + } + return OkStatus(); + } + case USD: { + return Status(Status::DRACO_ERROR, "USD is not supported yet."); + } + case PLY: + case OBJ: { + // Convert the scene to mesh and save the scene as a mesh. For now we do + // that by converting the scene to GLB and decoding the GLB into a mesh. + GltfEncoder gltf_encoder; + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(gltf_encoder.EncodeToBuffer(scene, &buffer)); + GltfDecoder gltf_decoder; + DecoderBuffer dec_buffer; + dec_buffer.Init(buffer.data(), buffer.size()); + DRACO_ASSIGN_OR_RETURN(auto mesh, + gltf_decoder.DecodeFromBuffer(&dec_buffer)); + if (format == PLY) { + PlyEncoder ply_encoder; + if (!ply_encoder.EncodeToFile(*mesh, file_name)) { + return ErrorStatus("Failed to encode the scene as PLY."); + } + } + if (format == OBJ) { + ObjEncoder obj_encoder; + if (!obj_encoder.EncodeToFile(*mesh, file_name)) { + return ErrorStatus("Failed to encode the scene as OBJ."); + } + } + return OkStatus(); + } + default: { + return Status(Status::DRACO_ERROR, "Unknown output file format."); + } + } +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/scene_io.h b/contrib/draco/src/draco/io/scene_io.h new file mode 100644 index 000000000..964faac3c --- /dev/null +++ b/contrib/draco/src/draco/io/scene_io.h @@ -0,0 +1,55 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_SCENE_IO_H_ +#define DRACO_IO_SCENE_IO_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/options.h" +#include "draco/core/status_or.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Reads a scene from a file. Currently only GLTF 2.0 scene files are supported. +// The second form returns the files associated with the scene via the +// |scene_files| argument. +StatusOr> ReadSceneFromFile( + const std::string &file_name); +StatusOr> ReadSceneFromFile( + const std::string &file_name, std::vector *scene_files); + +// Writes a scene into a file. +Status WriteSceneToFile(const std::string &file_name, const Scene &scene); + +// Writes a scene into a file, configurable with |options|. +// +// Supported options: +// +// force_usd_vertex_interpolation= - forces implicit vertex +// interpolation while exporting to USD +// (default = false) +// +Status WriteSceneToFile(const std::string &file_name, const Scene &scene, + const Options &options); + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_SCENE_IO_H_ diff --git a/contrib/draco/src/draco/io/scene_io_test.cc b/contrib/draco/src/draco/io/scene_io_test.cc new file mode 100644 index 000000000..828065693 --- /dev/null +++ b/contrib/draco/src/draco/io/scene_io_test.cc @@ -0,0 +1,86 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/scene_io.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_utils.h" +#include "draco/io/mesh_io.h" + +namespace { + +TEST(SceneTest, TestSceneIO) { + // A simple test that verifies that the scene is loaded and saved using the + // scene_io.h API. + const std::string file_name = + draco::GetTestFileFullPath("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + draco::StatusOr> maybe_scene = + draco::ReadSceneFromFile(file_name); + ASSERT_TRUE(maybe_scene.status().ok()); + std::unique_ptr scene = std::move(maybe_scene).value(); + ASSERT_NE(scene, nullptr); + + const std::string out_file_name = + draco::GetTestTempFileFullPath("out_scene.gltf"); + ASSERT_TRUE(draco::WriteSceneToFile(out_file_name, *scene).ok()); + + // Ensure all files related to the scene are saved. + ASSERT_GT(draco::GetFileSize(out_file_name), 0); + ASSERT_GT( + draco::GetFileSize(draco::GetTestTempFileFullPath("CesiumMilkTruck.png")), + 0); + ASSERT_GT(draco::GetFileSize(draco::GetTestTempFileFullPath("buffer0.bin")), + 0); +} + +TEST(SceneTest, TestSaveToPly) { + // A simple test that verifies that a loaded scene can be stored in a PLY file + // format. + const std::string file_name = + draco::GetTestFileFullPath("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr scene, + draco::ReadSceneFromFile(file_name)); + + const std::string out_file_name = + draco::GetTestTempFileFullPath("out_scene.ply"); + DRACO_ASSERT_OK(draco::WriteSceneToFile(out_file_name, *scene)); + + // Verify that we can read the saved mesh. + DRACO_ASSIGN_OR_ASSERT(auto mesh, draco::ReadMeshFromFile(out_file_name)); + ASSERT_NE(mesh, nullptr); +} + +TEST(SceneTest, TestSaveToObj) { + // A simple test that verifies that a loaded scene can be stored in an OBJ + // file format. + const std::string file_name = + draco::GetTestFileFullPath("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr scene, + draco::ReadSceneFromFile(file_name)); + + const std::string out_file_name = + draco::GetTestTempFileFullPath("out_scene.obj"); + DRACO_ASSERT_OK(draco::WriteSceneToFile(out_file_name, *scene)); + + // Verify that we can read the saved mesh. + DRACO_ASSIGN_OR_ASSERT(auto mesh, draco::ReadMeshFromFile(out_file_name)); + ASSERT_NE(mesh, nullptr); +} + +} // namespace +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/stdio_file_reader_test.cc b/contrib/draco/src/draco/io/stdio_file_reader_test.cc index 487819a02..212945f90 100644 --- a/contrib/draco/src/draco/io/stdio_file_reader_test.cc +++ b/contrib/draco/src/draco/io/stdio_file_reader_test.cc @@ -9,7 +9,7 @@ namespace { TEST(StdioFileReaderTest, FailOpen) { EXPECT_EQ(StdioFileReader::Open(""), nullptr); - EXPECT_EQ(StdioFileReader::Open("fake file"), nullptr); + EXPECT_EQ(StdioFileReader::Open("stdio reader fake file"), nullptr); } TEST(StdioFileReaderTest, Open) { diff --git a/contrib/draco/src/draco/io/stl_decoder.cc b/contrib/draco/src/draco/io/stl_decoder.cc new file mode 100644 index 000000000..1e5d3a938 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_decoder.cc @@ -0,0 +1,77 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/stl_decoder.h" + +#include + +#include "draco/core/macros.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/file_utils.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" + +namespace draco { + +StatusOr> StlDecoder::DecodeFromFile( + const std::string &file_name) { + std::vector data; + if (!ReadFileToBuffer(file_name, &data)) { + return Status(Status::IO_ERROR, "Unable to read input file."); + } + DecoderBuffer buffer; + buffer.Init(data.data(), data.size()); + return DecodeFromBuffer(&buffer); +} + +StatusOr> StlDecoder::DecodeFromBuffer( + DecoderBuffer *buffer) { + if (!strncmp(buffer->data_head(), "solid ", 6)) { + return Status(Status::IO_ERROR, + "Currently only binary STL files are supported."); + } + buffer->Advance(80); + uint32_t face_count; + buffer->Decode(&face_count, 4); + + TriangleSoupMeshBuilder builder; + builder.Start(face_count); + + const int32_t pos_att_id = + builder.AddAttribute(GeometryAttribute::POSITION, 3, DT_FLOAT32); + const int32_t norm_att_id = + builder.AddAttribute(GeometryAttribute::NORMAL, 3, DT_FLOAT32); + + for (uint32_t i = 0; i < face_count; i++) { + float data[48]; + buffer->Decode(data, 48); + uint16_t unused; + buffer->Decode(&unused, 2); + + builder.SetPerFaceAttributeValueForFace( + norm_att_id, draco::FaceIndex(i), + draco::Vector3f(data[0], data[1], data[2]).data()); + + builder.SetAttributeValuesForFace( + pos_att_id, draco::FaceIndex(i), + draco::Vector3f(data[3], data[4], data[5]).data(), + draco::Vector3f(data[6], data[7], data[8]).data(), + draco::Vector3f(data[9], data[10], data[11]).data()); + } + + std::unique_ptr mesh = builder.Finalize(); + return mesh; +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/stl_decoder.h b/contrib/draco/src/draco/io/stl_decoder.h new file mode 100644 index 000000000..44b35d849 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_decoder.h @@ -0,0 +1,38 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_STL_DECODER_H_ +#define DRACO_IO_STL_DECODER_H_ + +#include + +#include "draco/core/decoder_buffer.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/draco_features.h" +#include "draco/mesh/mesh.h" + +namespace draco { + +// Decodes an STL file into draco::Mesh (or draco::PointCloud if the +// connectivity data is not needed). +class StlDecoder { + public: + StatusOr> DecodeFromFile(const std::string &file_name); + StatusOr> DecodeFromBuffer(DecoderBuffer *buffer); +}; + +} // namespace draco + +#endif // DRACO_IO_STL_DECODER_H_ diff --git a/contrib/draco/src/draco/io/stl_decoder_test.cc b/contrib/draco/src/draco/io/stl_decoder_test.cc new file mode 100644 index 000000000..886881925 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_decoder_test.cc @@ -0,0 +1,49 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/stl_decoder.h" + +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace draco { + +class StlDecoderTest : public ::testing::Test { + protected: + void test_decoding(const std::string &file_name) { + const std::string path = GetTestFileFullPath(file_name); + StlDecoder decoder; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr mesh, + decoder.DecodeFromFile(path)); + ASSERT_GT(mesh->num_faces(), 0); + ASSERT_GT(mesh->num_points(), 0); + } + + void test_decoding_should_fail(const std::string &file_name) { + StlDecoder decoder; + StatusOr> statusOrMesh = + decoder.DecodeFromFile(GetTestFileFullPath(file_name)); + ASSERT_FALSE(statusOrMesh.ok()); + } +}; + +TEST_F(StlDecoderTest, TestStlDecoding) { + test_decoding("STL/bunny.stl"); + test_decoding("STL/test_sphere.stl"); + test_decoding_should_fail("STL/test_sphere_ascii.stl"); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/stl_encoder.cc b/contrib/draco/src/draco/io/stl_encoder.cc new file mode 100644 index 000000000..5aa4a0a97 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_encoder.cc @@ -0,0 +1,111 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/stl_encoder.h" + +#include +#include +#include +#include + +#include "draco/io/file_writer_factory.h" +#include "draco/io/file_writer_interface.h" + +namespace draco { + +StlEncoder::StlEncoder() + : out_buffer_(nullptr), in_point_cloud_(nullptr), in_mesh_(nullptr) {} + +Status StlEncoder::EncodeToFile(const Mesh &mesh, + const std::string &file_name) { + in_mesh_ = &mesh; + std::unique_ptr file = + FileWriterFactory::OpenWriter(file_name); + if (!file) { + return Status(Status::IO_ERROR, "File couldn't be opened"); + } + // Encode the mesh into a buffer. + EncoderBuffer buffer; + DRACO_RETURN_IF_ERROR(EncodeToBuffer(mesh, &buffer)); + // Write the buffer into the file. + file->Write(buffer.data(), buffer.size()); + return OkStatus(); +} + +Status StlEncoder::EncodeToBuffer(const Mesh &mesh, EncoderBuffer *out_buffer) { + in_mesh_ = &mesh; + out_buffer_ = out_buffer; + Status s = EncodeInternal(); + in_mesh_ = nullptr; // cleanup + in_point_cloud_ = nullptr; + out_buffer_ = nullptr; + return s; +} + +Status StlEncoder::EncodeInternal() { + // Write STL header. + std::stringstream out; + out << std::left << std::setw(80) + << "generated using Draco"; // header is 80 bytes fixed size. + const std::string header_str = out.str(); + buffer()->Encode(header_str.data(), header_str.length()); + + uint32_t num_faces = in_mesh_->num_faces(); + buffer()->Encode(&num_faces, 4); + + std::vector stl_face; + + const int pos_att_id = + in_mesh_->GetNamedAttributeId(GeometryAttribute::POSITION); + + if (pos_att_id < 0) { + return ErrorStatus("Mesh is missing the position attribute."); + } + + if (in_mesh_->attribute(pos_att_id)->data_type() != DT_FLOAT32) { + return ErrorStatus("Mesh position attribute is not of type float32."); + } + + uint16_t unused = 0; + + if (in_mesh_) { + for (FaceIndex i(0); i < in_mesh_->num_faces(); ++i) { + const auto &f = in_mesh_->face(i); + const auto *const pos_att = in_mesh_->attribute(pos_att_id); + + // The normal attribute can contain arbitrary normals that may not + // correspond to the winding of the face. + // Therefor we simply always calculate them + // using the points of the triangle face: norm(cross(p2-p1, p3-p1)) + + Vector3f pos[3]; + pos_att->GetMappedValue(f[0], &pos[0][0]); + pos_att->GetMappedValue(f[1], &pos[1][0]); + pos_att->GetMappedValue(f[2], &pos[2][0]); + Vector3f norm = CrossProduct(pos[1] - pos[0], pos[2] - pos[0]); + norm.Normalize(); + buffer()->Encode(norm.data(), sizeof(float) * 3); + + for (int c = 0; c < 3; ++c) { + buffer()->Encode(pos_att->GetAddress(pos_att->mapped_index(f[c])), + pos_att->byte_stride()); + } + + buffer()->Encode(&unused, 2); + } + } + return OkStatus(); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/stl_encoder.h b/contrib/draco/src/draco/io/stl_encoder.h new file mode 100644 index 000000000..8b185b738 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_encoder.h @@ -0,0 +1,52 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_STL_ENCODER_H_ +#define DRACO_IO_STL_ENCODER_H_ + +#include + +#include "draco/core/encoder_buffer.h" +#include "draco/draco_features.h" +#include "draco/mesh/mesh.h" + +namespace draco { + +// Class for encoding draco::Mesh into the STL file format. +class StlEncoder { + public: + StlEncoder(); + + // Encodes the mesh and saves it into a file. + // Returns false when either the encoding failed or when the file couldn't be + // opened. + Status EncodeToFile(const Mesh &mesh, const std::string &file_name); + + // Encodes the mesh into a buffer. + Status EncodeToBuffer(const Mesh &mesh, EncoderBuffer *out_buffer); + + protected: + Status EncodeInternal(); + EncoderBuffer *buffer() const { return out_buffer_; } + + private: + EncoderBuffer *out_buffer_; + + const PointCloud *in_point_cloud_; + const Mesh *in_mesh_; +}; + +} // namespace draco + +#endif // DRACO_IO_STL_ENCODER_H_ diff --git a/contrib/draco/src/draco/io/stl_encoder_test.cc b/contrib/draco/src/draco/io/stl_encoder_test.cc new file mode 100644 index 000000000..da6298d64 --- /dev/null +++ b/contrib/draco/src/draco/io/stl_encoder_test.cc @@ -0,0 +1,78 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/stl_encoder.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_reader_factory.h" +#include "draco/io/file_reader_interface.h" +#include "draco/io/stl_decoder.h" + +namespace draco { + +class StlEncoderTest : public ::testing::Test { + protected: + void CompareMeshes(const Mesh *mesh0, const Mesh *mesh1) { + ASSERT_EQ(mesh0->num_faces(), mesh1->num_faces()); + ASSERT_EQ(mesh0->num_attributes(), mesh1->num_attributes()); + for (size_t att_id = 0; att_id < mesh0->num_attributes(); ++att_id) { + ASSERT_EQ(mesh0->attribute(att_id)->size(), + mesh1->attribute(att_id)->size()); + } + } + + // Encode a mesh using the StlEncoder and then decode to verify the encoding. + std::unique_ptr EncodeAndDecodeMesh(const Mesh *mesh) { + EncoderBuffer encoder_buffer; + StlEncoder encoder; + Status status = encoder.EncodeToBuffer(*mesh, &encoder_buffer); + if (!status.ok()) { + return nullptr; + } + + DecoderBuffer decoder_buffer; + decoder_buffer.Init(encoder_buffer.data(), encoder_buffer.size()); + StlDecoder decoder; + StatusOr> status_or_mesh = + decoder.DecodeFromBuffer(&decoder_buffer); + if (!status_or_mesh.ok()) { + return nullptr; + } + std::unique_ptr decoded_mesh = std::move(status_or_mesh).value(); + return decoded_mesh; + } + + void test_encoding(const std::string &file_name) { + const std::unique_ptr mesh(ReadMeshFromTestFile(file_name, true)); + + ASSERT_NE(mesh, nullptr) << "Failed to load test model " << file_name; + ASSERT_GT(mesh->num_faces(), 0); + + const std::unique_ptr decoded_mesh = EncodeAndDecodeMesh(mesh.get()); + CompareMeshes(mesh.get(), decoded_mesh.get()); + } +}; + +TEST_F(StlEncoderTest, TestStlEncoding) { + // Test decoded mesh from encoded stl file stays the same. + test_encoding("STL/bunny.stl"); + test_encoding("STL/test_sphere.stl"); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/io/texture_io.cc b/contrib/draco/src/draco/io/texture_io.cc new file mode 100644 index 000000000..cbe2915b0 --- /dev/null +++ b/contrib/draco/src/draco/io/texture_io.cc @@ -0,0 +1,94 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/texture_io.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include "draco/io/file_utils.h" + +namespace draco { + +namespace { + +StatusOr> CreateDracoTextureInternal( + const std::vector &image_data, SourceImage *out_source_image) { + std::unique_ptr draco_texture(new Texture()); + out_source_image->MutableEncodedData() = image_data; + return std::move(draco_texture); +} + +} // namespace + +StatusOr> ReadTextureFromFile( + const std::string &file_name) { + std::vector image_data; + if (!ReadFileToBuffer(file_name, &image_data)) { + return Status(Status::IO_ERROR, "Unable to read input texture file."); + } + + SourceImage source_image; + DRACO_ASSIGN_OR_RETURN(auto texture, + CreateDracoTextureInternal(image_data, &source_image)); + source_image.set_filename(file_name); + const std::string extension = LowercaseFileExtension(file_name); + const std::string mime_type = + "image/" + (extension == "jpg" ? "jpeg" : extension); + source_image.set_mime_type(mime_type); + texture->set_source_image(source_image); + return texture; +} + +StatusOr> ReadTextureFromBuffer( + const uint8_t *buffer, size_t buffer_size, const std::string &mime_type) { + SourceImage source_image; + std::vector image_data(buffer, buffer + buffer_size); + DRACO_ASSIGN_OR_RETURN(auto texture, + CreateDracoTextureInternal(image_data, &source_image)); + source_image.set_mime_type(mime_type); + texture->set_source_image(source_image); + return texture; +} + +Status WriteTextureToFile(const std::string &file_name, + const Texture &texture) { + std::vector buffer; + DRACO_RETURN_IF_ERROR(WriteTextureToBuffer(texture, &buffer)); + + if (!WriteBufferToFile(buffer.data(), buffer.size(), file_name)) { + return Status(Status::DRACO_ERROR, "Failed to write image."); + } + + return OkStatus(); +} + +Status WriteTextureToBuffer(const Texture &texture, + std::vector *buffer) { + // Copy data from the encoded source image if possible, otherwise load the + // data from the source file. + if (!texture.source_image().encoded_data().empty()) { + *buffer = texture.source_image().encoded_data(); + } else if (!texture.source_image().filename().empty()) { + if (!ReadFileToBuffer(texture.source_image().filename(), buffer)) { + return Status(Status::IO_ERROR, "Unable to read input texture file."); + } + } else { + return Status(Status::DRACO_ERROR, "Invalid source data for the texture."); + } + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/texture_io.h b/contrib/draco/src/draco/io/texture_io.h new file mode 100644 index 000000000..4dbea7554 --- /dev/null +++ b/contrib/draco/src/draco/io/texture_io.h @@ -0,0 +1,56 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_TEXTURE_IO_H_ +#define DRACO_IO_TEXTURE_IO_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/core/draco_types.h" +#include "draco/core/status_or.h" +#include "draco/texture/texture.h" + +namespace draco { + +// Reads a texture from a file. Reads PNG, JPEG and WEBP texture files. +// Returns nullptr with an error status if the decoding failed. +StatusOr> ReadTextureFromFile( + const std::string &file_name); + +// Same as ReadTextureFromFile() but the texture data is parsed from a |buffer|. +// |mime_type| should be set to a type of the texture encoded in |buffer|. +// Supported mime types are "image/jpeg", "image/png" and "image/webp". +// TODO(ostava): We should be able to get the mime type directly from the +// |buffer| but our image decoding library doesn't support this at this time. +StatusOr> ReadTextureFromBuffer( + const uint8_t *buffer, size_t buffer_size, const std::string &mime_type); + +// Writes a texture into a file. Can write PNG, JPEG, WEBP, and KTX2 (with Basis +// compression) texture files depending on the extension specified in +// |file_name| and image format specified in |texture|. Note that images with +// Basis compression can only be saved to files in KTX2 format and not to files +// with "basis" extension. Returns an error status if the writing failed. +Status WriteTextureToFile(const std::string &file_name, const Texture &texture); + +// Writes a |texture| into |buffer|. +Status WriteTextureToBuffer(const Texture &texture, + std::vector *buffer); + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_TEXTURE_IO_H_ diff --git a/contrib/draco/src/draco/io/texture_io_test.cc b/contrib/draco/src/draco/io/texture_io_test.cc new file mode 100644 index 000000000..13f36e44a --- /dev/null +++ b/contrib/draco/src/draco/io/texture_io_test.cc @@ -0,0 +1,55 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/texture_io.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_utils.h" + +namespace { + +// Tests loading of textures from a buffer. +TEST(TextureIoTest, TestLoadFromBuffer) { + const std::string file_name = draco::GetTestFileFullPath("test.png"); + std::vector image_data; + ASSERT_TRUE(draco::ReadFileToBuffer(file_name, &image_data)); + + DRACO_ASSIGN_OR_ASSERT( + std::unique_ptr texture, + draco::ReadTextureFromBuffer(image_data.data(), image_data.size(), + "image/png")); + ASSERT_NE(texture, nullptr); + + ASSERT_EQ(texture->source_image().mime_type(), "image/png"); + + // Re-encode the texture again to ensure the content hasn't changed. + std::vector encoded_buffer; + DRACO_ASSERT_OK(draco::WriteTextureToBuffer(*texture, &encoded_buffer)); + + ASSERT_EQ(image_data.size(), encoded_buffer.size()); + for (int i = 0; i < encoded_buffer.size(); ++i) { + ASSERT_EQ(image_data[i], encoded_buffer[i]); + } +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/tiny_gltf_utils.cc b/contrib/draco/src/draco/io/tiny_gltf_utils.cc new file mode 100644 index 000000000..d57e1093c --- /dev/null +++ b/contrib/draco/src/draco/io/tiny_gltf_utils.cc @@ -0,0 +1,230 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/io/tiny_gltf_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/animation/animation.h" +#include "draco/animation/node_animation_data.h" +#include "draco/core/status.h" +#include "draco/core/vector_d.h" +#include "tiny_gltf.h" + +namespace draco { + +int TinyGltfUtils::GetNumComponentsForType(int type) { + switch (type) { + case TINYGLTF_TYPE_SCALAR: + return 1; + case TINYGLTF_TYPE_VEC2: + return 2; + case TINYGLTF_TYPE_VEC3: + return 3; + case TINYGLTF_TYPE_VEC4: + case TINYGLTF_TYPE_MAT2: + return 4; + case TINYGLTF_TYPE_MAT3: + return 9; + case TINYGLTF_TYPE_MAT4: + return 16; + } + return 0; +} + +Material::TransparencyMode TinyGltfUtils::TextToMaterialMode( + const std::string &mode) { + if (mode == "MASK") { + return Material::TRANSPARENCY_MASK; + } else if (mode == "BLEND") { + return Material::TRANSPARENCY_BLEND; + } else { + return Material::TRANSPARENCY_OPAQUE; + } +} + +AnimationSampler::SamplerInterpolation +TinyGltfUtils::TextToSamplerInterpolation(const std::string &interpolation) { + if (interpolation == "STEP") { + return AnimationSampler::SamplerInterpolation::STEP; + } else if (interpolation == "CUBICSPLINE") { + return AnimationSampler::SamplerInterpolation::CUBICSPLINE; + } else { + return AnimationSampler::SamplerInterpolation::LINEAR; + } +} + +AnimationChannel::ChannelTransformation +TinyGltfUtils::TextToChannelTransformation(const std::string &path) { + if (path == "rotation") { + return AnimationChannel::ChannelTransformation::ROTATION; + } else if (path == "scale") { + return AnimationChannel::ChannelTransformation::SCALE; + } else if (path == "weights") { + return AnimationChannel::ChannelTransformation::WEIGHTS; + } else { + return AnimationChannel::ChannelTransformation::TRANSLATION; + } +} + +Status TinyGltfUtils::AddChannelToAnimation( + const tinygltf::Model &model, const tinygltf::Animation &input_animation, + const tinygltf::AnimationChannel &channel, int node_index, + Animation *animation) { + std::unique_ptr new_channel(new AnimationChannel()); + + const tinygltf::AnimationSampler &sampler = + input_animation.samplers[channel.sampler]; + // Add the sampler associated with the channel. + DRACO_RETURN_IF_ERROR( + TinyGltfUtils::AddSamplerToAnimation(model, sampler, animation)); + new_channel->sampler_index = animation->NumSamplers() - 1; + new_channel->target_index = node_index; + new_channel->transformation_type = + TinyGltfUtils::TextToChannelTransformation(channel.target_path); + + animation->AddChannel(std::move(new_channel)); + return OkStatus(); +} + +Status TinyGltfUtils::AddSamplerToAnimation( + const tinygltf::Model &model, const tinygltf::AnimationSampler &sampler, + Animation *animation) { + std::unique_ptr node_animation_data( + new NodeAnimationData()); + // TODO(fgalligan): Add support to not copy the accessor data if it is + // referenced more than once. Currently we duplicate all animation data so + // that it is referenced only once in the glTF file. + const tinygltf::Accessor &input_accessor = model.accessors[sampler.input]; + DRACO_RETURN_IF_ERROR(AddAccessorToAnimationData(model, input_accessor, + node_animation_data.get())); + animation->AddNodeAnimationData(std::move(node_animation_data)); + std::unique_ptr new_sampler(new AnimationSampler()); + new_sampler->input_index = animation->NumNodeAnimationData() - 1; + + node_animation_data.reset(new NodeAnimationData()); + const tinygltf::Accessor &output_accessor = model.accessors[sampler.output]; + DRACO_RETURN_IF_ERROR(AddAccessorToAnimationData(model, output_accessor, + node_animation_data.get())); + animation->AddNodeAnimationData(std::move(node_animation_data)); + new_sampler->output_index = animation->NumNodeAnimationData() - 1; + + new_sampler->interpolation_type = + TinyGltfUtils::TextToSamplerInterpolation(sampler.interpolation); + animation->AddSampler(std::move(new_sampler)); + return OkStatus(); +} + +// Specialization for returning the data from |accessor| as a vector of float. +template <> +StatusOr> TinyGltfUtils::CopyDataAsFloat( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + const int num_components = GetNumComponentsForType(accessor.type); + if (num_components != 1) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + return CopyDataAsFloatImpl(model, accessor); +} + +// Specialization for returing the data from |accessor| as a vector of +// Matrix4x4. +template <> +StatusOr> TinyGltfUtils::CopyDataAsFloat( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + const int num_components = GetNumComponentsForType(accessor.type); + if (num_components != 16) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + return CopyDataAsFloatImpl(model, accessor); +} + +Status TinyGltfUtils::AddAccessorToAnimationData( + const tinygltf::Model &model, const tinygltf::Accessor &accessor, + NodeAnimationData *node_animation_data) { + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { + return Status(Status::DRACO_ERROR, + "Unsupported ComponentType for NodeAnimationData."); + } + + std::vector *dest_data = node_animation_data->GetMutableData(); + if (accessor.type == TINYGLTF_TYPE_SCALAR) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + dest_data->push_back(data[i]); + } + node_animation_data->SetType(NodeAnimationData::Type::SCALAR); + } else if (accessor.type == TINYGLTF_TYPE_VEC3) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + for (int j = 0; j < 3; ++j) { + dest_data->push_back(data[i][j]); + } + } + node_animation_data->SetType(NodeAnimationData::Type::VEC3); + } else if (accessor.type == TINYGLTF_TYPE_VEC4) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + for (int j = 0; j < 4; ++j) { + dest_data->push_back(data[i][j]); + } + } + node_animation_data->SetType(NodeAnimationData::Type::VEC4); + } else if (accessor.type == TINYGLTF_TYPE_MAT4) { + DRACO_ASSIGN_OR_RETURN(std::vector data, + CopyDataAsFloat(model, accessor)); + + for (int i = 0; i < data.size(); ++i) { + for (int j = 0; j < 16; ++j) { + dest_data->push_back(data[i](j)); + } + } + node_animation_data->SetType(NodeAnimationData::Type::MAT4); + } else { + return Status(Status::DRACO_ERROR, + "Unsupported Type for GltfNodeAnimationData."); + } + node_animation_data->SetCount(accessor.count); + node_animation_data->SetNormalized(accessor.normalized); + return OkStatus(); +} + +template <> +void TinyGltfUtils::SetDataImpl(float value, int index, float *values) { + *values = value; +} + +template <> +void TinyGltfUtils::SetDataImpl(float value, int index, + Eigen::Matrix4f *values) { + (*values)(index) = value; +} + +} // namespace draco + +// Actual definitions needed by the tinygltf library using our configuration. +#define STB_IMAGE_IMPLEMENTATION +#define STB_IMAGE_WRITE_IMPLEMENTATION +#define TINYGLTF_ENABLE_DRACO +#define TINYGLTF_IMPLEMENTATION + +#include "tiny_gltf.h" + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/io/tiny_gltf_utils.h b/contrib/draco/src/draco/io/tiny_gltf_utils.h new file mode 100644 index 000000000..a536a70fb --- /dev/null +++ b/contrib/draco/src/draco/io/tiny_gltf_utils.h @@ -0,0 +1,140 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_IO_TINY_GLTF_UTILS_H_ +#define DRACO_IO_TINY_GLTF_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "Eigen/Geometry" +#include "draco/animation/animation.h" +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/material/material.h" + +#define TINYGLTF_ENCLOSING_NAMESPACE draco +#include "tiny_gltf.h" + +namespace draco { + +class TinyGltfUtils { + public: + TinyGltfUtils() {} + + // Returns the number of components for the attribute type. + static int GetNumComponentsForType(int type); + + // Returns the material transparency mode in |mode|. + static Material::TransparencyMode TextToMaterialMode(const std::string &mode); + + // Returns the animation sampler interpolation in |interpolation|. + static AnimationSampler::SamplerInterpolation TextToSamplerInterpolation( + const std::string &interpolation); + + // Returns the animation channel transformation in |path|. + static AnimationChannel::ChannelTransformation TextToChannelTransformation( + const std::string &path); + + // Adds all of the animation data associated with a channel. + // The channel references a sampler, whose data will be added to the + // |animation|. The sampler references input and output accessors, + // whose data will be added to the |animation|. + static Status AddChannelToAnimation( + const tinygltf::Model &model, const tinygltf::Animation &input_animation, + const tinygltf::AnimationChannel &channel, int node_index, + Animation *animation); + + // Adds all of the sampler data. The sampler references + // input and output accessors, whose data will be added to the |animation|. + static Status AddSamplerToAnimation(const tinygltf::Model &model, + const tinygltf::AnimationSampler &sampler, + Animation *animation); + + // Converts the gltf2 animation accessor and adds it to + // |node_animation_data|. + static Status AddAccessorToAnimationData( + const tinygltf::Model &model, const tinygltf::Accessor &accessor, + NodeAnimationData *node_animation_data); + + // Returns the data from |accessor| as a vector of |T|. + template + static StatusOr> CopyDataAsFloat( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + const int num_components = GetNumComponentsForType(accessor.type); + if (num_components != T::dimension) { + return Status(Status::DRACO_ERROR, + "Dimension does not equal num components."); + } + return CopyDataAsFloatImpl(model, accessor); + } + + private: + template + static StatusOr> CopyDataAsFloatImpl( + const tinygltf::Model &model, const tinygltf::Accessor &accessor) { + if (accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) { + return Status(Status::DRACO_ERROR, + "Non-float data is not supported by CopyDataAsFloat()."); + } + if (accessor.bufferView < 0) { + return Status(Status::DRACO_ERROR, + "Error CopyDataAsFloat() bufferView < 0."); + } + + const tinygltf::BufferView &buffer_view = + model.bufferViews[accessor.bufferView]; + if (buffer_view.buffer < 0) { + return Status(Status::DRACO_ERROR, "Error CopyDataAsFloat() buffer < 0."); + } + + const tinygltf::Buffer &buffer = model.buffers[buffer_view.buffer]; + + const unsigned char *const data_start = + buffer.data.data() + buffer_view.byteOffset + accessor.byteOffset; + const int byte_stride = accessor.ByteStride(buffer_view); + const int component_size = + tinygltf::GetComponentSizeInBytes(accessor.componentType); + + std::vector output; + output.resize(accessor.count); + + const int num_components = GetNumComponentsForType(accessor.type); + const unsigned char *data = data_start; + for (int i = 0; i < accessor.count; ++i) { + T values; + + for (int c = 0; c < num_components; ++c) { + float value = 0.0f; + memcpy(&value, data + (c * component_size), component_size); + SetDataImpl(value, c, &values); + } + + output[i] = values; + data += byte_stride; + } + + return output; + } + + template + static void SetDataImpl(float value, int index, T *values) { + (*values)[index] = value; + } +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_IO_TINY_GLTF_UTILS_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.cc deleted file mode 100644 index 7e9e6d15d..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.cc +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -#include "draco/javascript/emscripten/animation_decoder_webidl_wrapper.h" - -#include - -#include "draco/compression/decode.h" -#include "draco/mesh/mesh.h" -#include "draco/mesh/mesh_stripifier.h" - -using draco::DecoderBuffer; -using draco::PointAttribute; -using draco::Status; - -DracoFloat32Array::DracoFloat32Array() {} - -float DracoFloat32Array::GetValue(int index) const { return values_[index]; } - -bool DracoFloat32Array::SetValues(const float *values, int count) { - if (values) { - values_.assign(values, values + count); - } else { - values_.resize(count); - } - return true; -} - -AnimationDecoder::AnimationDecoder() {} - -// Decodes animation data from the provided buffer. -const draco::Status *AnimationDecoder::DecodeBufferToKeyframeAnimation( - draco::DecoderBuffer *in_buffer, draco::KeyframeAnimation *animation) { - draco::DecoderOptions dec_options; - last_status_ = decoder_.Decode(dec_options, in_buffer, animation); - return &last_status_; -} - -bool AnimationDecoder::GetTimestamps(const draco::KeyframeAnimation &animation, - DracoFloat32Array *timestamp) { - if (!timestamp) { - return false; - } - const int num_frames = animation.num_frames(); - const draco::PointAttribute *timestamp_att = animation.timestamps(); - // Timestamp attribute has only 1 component, so the number of components is - // equal to the number of frames. - timestamp->SetValues(nullptr, num_frames); - int entry_id = 0; - float timestamp_value = -1.0; - for (draco::PointIndex i(0); i < num_frames; ++i) { - const draco::AttributeValueIndex val_index = timestamp_att->mapped_index(i); - if (!timestamp_att->ConvertValue(val_index, ×tamp_value)) { - return false; - } - timestamp->SetValue(entry_id++, timestamp_value); - } - return true; -} - -bool AnimationDecoder::GetKeyframes(const draco::KeyframeAnimation &animation, - int keyframes_id, - DracoFloat32Array *animation_data) { - const int num_frames = animation.num_frames(); - // Get animation data. - const draco::PointAttribute *animation_data_att = - animation.keyframes(keyframes_id); - if (!animation_data_att) { - return false; - } - - const int components = animation_data_att->num_components(); - const int num_entries = num_frames * components; - const int kMaxAttributeFloatValues = 4; - - std::vector values(components, -1.0); - int entry_id = 0; - animation_data->SetValues(nullptr, num_entries); - for (draco::PointIndex i(0); i < num_frames; ++i) { - const draco::AttributeValueIndex val_index = - animation_data_att->mapped_index(i); - if (!animation_data_att->ConvertValue(val_index, &values[0])) { - return false; - } - for (int j = 0; j < components; ++j) { - animation_data->SetValue(entry_id++, values[j]); - } - } - return true; -} diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.h b/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.h deleted file mode 100644 index 7486d1503..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_decoder_webidl_wrapper.h +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -#ifndef DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_DECODER_WEBIDL_WRAPPER_H_ -#define DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_DECODER_WEBIDL_WRAPPER_H_ - -#include - -#include "draco/animation/keyframe_animation_decoder.h" -#include "draco/attributes/attribute_transform_type.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/config/compression_shared.h" -#include "draco/compression/decode.h" -#include "draco/core/decoder_buffer.h" - -typedef draco::AttributeTransformType draco_AttributeTransformType; -typedef draco::GeometryAttribute draco_GeometryAttribute; -typedef draco_GeometryAttribute::Type draco_GeometryAttribute_Type; -typedef draco::EncodedGeometryType draco_EncodedGeometryType; -typedef draco::Status draco_Status; -typedef draco::Status::Code draco_StatusCode; - -class DracoFloat32Array { - public: - DracoFloat32Array(); - float GetValue(int index) const; - - // In case |values| is nullptr, the data is allocated but not initialized. - bool SetValues(const float *values, int count); - - // Directly sets a value for a specific index. The array has to be already - // allocated at this point (using SetValues() method). - void SetValue(int index, float val) { values_[index] = val; } - - int size() const { return values_.size(); } - - private: - std::vector values_; -}; - -// Class used by emscripten WebIDL Binder [1] to wrap calls to decode animation -// data. -class AnimationDecoder { - public: - AnimationDecoder(); - - // Decodes animation data from the provided buffer. - const draco::Status *DecodeBufferToKeyframeAnimation( - draco::DecoderBuffer *in_buffer, draco::KeyframeAnimation *animation); - - static bool GetTimestamps(const draco::KeyframeAnimation &animation, - DracoFloat32Array *timestamp); - - static bool GetKeyframes(const draco::KeyframeAnimation &animation, - int keyframes_id, DracoFloat32Array *animation_data); - - private: - draco::KeyframeAnimationDecoder decoder_; - draco::Status last_status_; -}; - -#endif // DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_DECODER_WEBIDL_WRAPPER_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.cc deleted file mode 100644 index 53a10e5e4..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.cc +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -#include "draco/javascript/emscripten/animation_encoder_webidl_wrapper.h" - -#include "draco/animation/keyframe_animation.h" -#include "draco/animation/keyframe_animation_encoder.h" - -DracoInt8Array::DracoInt8Array() {} - -int DracoInt8Array::GetValue(int index) const { return values_[index]; } - -bool DracoInt8Array::SetValues(const char *values, int count) { - values_.assign(values, values + count); - return true; -} - -AnimationBuilder::AnimationBuilder() {} - -bool AnimationBuilder::SetTimestamps(draco::KeyframeAnimation *animation, - long num_frames, const float *timestamps) { - if (!animation || !timestamps) { - return false; - } - std::vector timestamps_arr( - timestamps, timestamps + num_frames); - return animation->SetTimestamps(timestamps_arr); -} - -int AnimationBuilder::AddKeyframes(draco::KeyframeAnimation *animation, - long num_frames, long num_components, - const float *animation_data) { - if (!animation || !animation_data) { - return -1; - } - std::vector keyframes_arr( - animation_data, animation_data + num_frames * num_components); - return animation->AddKeyframes(draco::DT_FLOAT32, num_components, - keyframes_arr); -} - -AnimationEncoder::AnimationEncoder() - : timestamps_quantization_bits_(-1), - keyframes_quantization_bits_(-1), - options_(draco::EncoderOptions::CreateDefaultOptions()) {} - -void AnimationEncoder::SetTimestampsQuantization(long quantization_bits) { - timestamps_quantization_bits_ = quantization_bits; -} - -void AnimationEncoder::SetKeyframesQuantization(long quantization_bits) { - keyframes_quantization_bits_ = quantization_bits; -} - -int AnimationEncoder::EncodeAnimationToDracoBuffer( - draco::KeyframeAnimation *animation, DracoInt8Array *draco_buffer) { - if (!animation) { - return 0; - } - draco::EncoderBuffer buffer; - - if (timestamps_quantization_bits_ > 0) { - options_.SetAttributeInt(0, "quantization_bits", - timestamps_quantization_bits_); - } - if (keyframes_quantization_bits_ > 0) { - for (int i = 1; i <= animation->num_animations(); ++i) { - options_.SetAttributeInt(i, "quantization_bits", - keyframes_quantization_bits_); - } - } - if (!encoder_.EncodeKeyframeAnimation(*animation, options_, &buffer).ok()) { - return 0; - } - - draco_buffer->SetValues(buffer.data(), buffer.size()); - return buffer.size(); -} diff --git a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.h b/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.h deleted file mode 100644 index f2ac733d1..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/animation_encoder_webidl_wrapper.h +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -#ifndef DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_ENCODER_WEBIDL_WRAPPER_H_ -#define DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_ENCODER_WEBIDL_WRAPPER_H_ - -#include - -#include "draco/animation/keyframe_animation_encoder.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/config/compression_shared.h" -#include "draco/compression/config/encoder_options.h" -#include "draco/compression/encode.h" - -class DracoInt8Array { - public: - DracoInt8Array(); - int GetValue(int index) const; - bool SetValues(const char *values, int count); - - size_t size() { return values_.size(); } - - private: - std::vector values_; -}; - -class AnimationBuilder { - public: - AnimationBuilder(); - - bool SetTimestamps(draco::KeyframeAnimation *animation, long num_frames, - const float *timestamps); - - int AddKeyframes(draco::KeyframeAnimation *animation, long num_frames, - long num_components, const float *animation_data); -}; - -class AnimationEncoder { - public: - AnimationEncoder(); - - void SetTimestampsQuantization(long quantization_bits); - // TODO: Use expert encoder to set per attribute quantization. - void SetKeyframesQuantization(long quantization_bits); - int EncodeAnimationToDracoBuffer(draco::KeyframeAnimation *animation, - DracoInt8Array *draco_buffer); - - private: - draco::KeyframeAnimationEncoder encoder_; - long timestamps_quantization_bits_; - long keyframes_quantization_bits_; - draco::EncoderOptions options_; -}; - -#endif // DRACO_JAVASCRIPT_EMSCRIPTEN_ANIMATION_ENCODER_WEBIDL_WRAPPER_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc index 66fe77dbd..034f3c3b4 100644 --- a/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc +++ b/contrib/draco/src/draco/javascript/emscripten/decoder_webidl_wrapper.cc @@ -221,14 +221,13 @@ bool Decoder::GetAttributeFloatForAllPoints(const PointCloud &pc, const int components = pa.num_components(); const int num_points = pc.num_points(); const int num_entries = num_points * components; - const int kMaxAttributeFloatValues = 4; - float values[kMaxAttributeFloatValues] = {-2.0, -2.0, -2.0, -2.0}; + std::vector values(components, -2.f); int entry_id = 0; out_values->Resize(num_entries); for (draco::PointIndex i(0); i < num_points; ++i) { const draco::AttributeValueIndex val_index = pa.mapped_index(i); - if (!pa.ConvertValue(val_index, values)) { + if (!pa.ConvertValue(val_index, &values[0])) { return false; } for (int j = 0; j < components; ++j) { @@ -249,17 +248,16 @@ bool Decoder::GetAttributeFloatArrayForAllPoints(const PointCloud &pc, return false; } const bool requested_type_is_float = pa.data_type() == draco::DT_FLOAT32; - const int kMaxAttributeFloatValues = 4; - float values[kMaxAttributeFloatValues] = {-2.0, -2.0, -2.0, -2.0}; + std::vector values(components, -2.f); int entry_id = 0; float *const floats = reinterpret_cast(out_values); for (draco::PointIndex i(0); i < num_points; ++i) { const draco::AttributeValueIndex val_index = pa.mapped_index(i); if (requested_type_is_float) { - pa.GetValue(val_index, values); + pa.GetValue(val_index, &values[0]); } else { - if (!pa.ConvertValue(val_index, values)) { + if (!pa.ConvertValue(val_index, &values[0])) { return false; } } diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc b/contrib/draco/src/draco/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc deleted file mode 100644 index 83ed98fdc..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_decoder_glue_wrapper.cc +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2017 The Draco Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// This file is used by emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -#include "draco/attributes/attribute_octahedron_transform.h" -#include "draco/attributes/attribute_quantization_transform.h" -#include "draco/attributes/geometry_attribute.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/decode.h" -#include "draco/core/decoder_buffer.h" -#include "draco/javascript/emscripten/animation_decoder_webidl_wrapper.h" -#include "draco/mesh/mesh.h" -#include "draco/point_cloud/point_cloud.h" - -// glue_animation_decoder.cpp is generated by Makefile.emcc build_glue target. -#include "glue_animation_decoder.cpp" diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_decoder.idl b/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_decoder.idl deleted file mode 100644 index c9fe76b59..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_decoder.idl +++ /dev/null @@ -1,52 +0,0 @@ -// Interface exposed to emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -[Prefix="draco::"] -interface DecoderBuffer { - void DecoderBuffer(); - void Init([Const] byte[] data, unsigned long data_size); -}; - -enum draco_StatusCode { - "draco_Status::OK", - "draco_Status::DRACO_ERROR", - "draco_Status::IO_ERROR", - "draco_Status::INVALID_PARAMETER", - "draco_Status::UNSUPPORTED_VERSION", - "draco_Status::UNKNOWN_VERSION", -}; - -[Prefix="draco::"] -interface Status { - draco_StatusCode code(); - boolean ok(); - [Const] DOMString error_msg(); -}; - -// Draco version of typed arrays. The memory of these arrays is allocated on the -// emscripten heap. -interface DracoFloat32Array { - void DracoFloat32Array(); - float GetValue(long index); - long size(); -}; - -[Prefix="draco::"] -interface KeyframeAnimation { - void KeyframeAnimation(); - long num_frames(); - long num_animations(); -}; - -interface AnimationDecoder { - void AnimationDecoder(); - - [Const] Status DecodeBufferToKeyframeAnimation(DecoderBuffer in_buffer, - KeyframeAnimation animation); - - boolean GetTimestamps([Ref, Const] KeyframeAnimation animation, - DracoFloat32Array timestamp); - - boolean GetKeyframes([Ref, Const] KeyframeAnimation animation, - long keyframes_id, - DracoFloat32Array animation_data); -}; diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_encoder.idl b/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_encoder.idl deleted file mode 100644 index e74a4c9e4..000000000 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_web_encoder.idl +++ /dev/null @@ -1,34 +0,0 @@ -// Interface exposed to emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -// Draco version of typed arrays. The memory of these arrays is allocated on the -// emscripten heap. -interface DracoInt8Array { - void DracoInt8Array(); - long GetValue(long index); - long size(); -}; - -[Prefix="draco::"] -interface KeyframeAnimation { - void KeyframeAnimation(); - long num_frames(); -}; - -interface AnimationBuilder { - void AnimationBuilder(); - boolean SetTimestamps(KeyframeAnimation animation, long num_frames, - [Const] float[] timestamps); - - long AddKeyframes(KeyframeAnimation animation, long num_frames, - long num_components, [Const] float[] animation_data); -}; - -interface AnimationEncoder { - void AnimationEncoder(); - - void SetTimestampsQuantization(long quantization_bits); - void SetKeyframesQuantization(long quantization_bits); - - long EncodeAnimationToDracoBuffer(KeyframeAnimation animation, - DracoInt8Array encoded_data); -}; diff --git a/contrib/draco/src/draco/javascript/emscripten/version.js b/contrib/draco/src/draco/javascript/emscripten/version.js index 46fb25271..b21f3b5e2 100644 --- a/contrib/draco/src/draco/javascript/emscripten/version.js +++ b/contrib/draco/src/draco/javascript/emscripten/version.js @@ -19,7 +19,7 @@ function isVersionSupported(versionString) { const version = versionString.split('.'); if (version.length < 2 || version.length > 3) return false; // Unexpected version string. - if (version[0] == 1 && version[1] >= 0 && version[1] <= 4) + if (version[0] == 1 && version[1] >= 0 && version[1] <= 5) return true; if (version[0] != 0 || version[1] > 10) return false; diff --git a/contrib/draco/src/draco/material/material.cc b/contrib/draco/src/draco/material/material.cc new file mode 100644 index 000000000..da854a172 --- /dev/null +++ b/contrib/draco/src/draco/material/material.cc @@ -0,0 +1,258 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/material/material.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +Material::Material() : Material(nullptr) {} + +Material::Material(TextureLibrary *texture_library) + : texture_library_(texture_library) { + Clear(); +} + +void Material::Copy(const Material &src) { + name_ = src.name_; + color_factor_ = src.color_factor_; + metallic_factor_ = src.metallic_factor_; + roughness_factor_ = src.roughness_factor_; + emissive_factor_ = src.emissive_factor_; + transparency_mode_ = src.transparency_mode_; + alpha_cutoff_ = src.alpha_cutoff_; + double_sided_ = src.double_sided_; + normal_texture_scale_ = src.normal_texture_scale_; + + // Copy properties of material extensions. + unlit_ = src.unlit_; + has_sheen_ = src.has_sheen_; + sheen_color_factor_ = src.sheen_color_factor_; + sheen_roughness_factor_ = src.sheen_roughness_factor_; + has_transmission_ = src.has_transmission_; + transmission_factor_ = src.transmission_factor_; + has_clearcoat_ = src.has_clearcoat_; + clearcoat_factor_ = src.clearcoat_factor_; + clearcoat_roughness_factor_ = src.clearcoat_roughness_factor_; + has_volume_ = src.has_volume_; + thickness_factor_ = src.thickness_factor_; + attenuation_distance_ = src.attenuation_distance_; + attenuation_color_ = src.attenuation_color_; + has_ior_ = src.has_ior_; + ior_ = src.ior_; + has_specular_ = src.has_specular_; + specular_factor_ = src.specular_factor_; + specular_color_factor_ = src.specular_color_factor_; + + // Copy texture maps. + texture_map_type_to_index_map_ = src.texture_map_type_to_index_map_; + texture_maps_.resize(src.texture_maps_.size()); + for (int i = 0; i < texture_maps_.size(); ++i) { + texture_maps_[i] = std::unique_ptr(new TextureMap()); + texture_maps_[i]->Copy(*src.texture_maps_[i]); + } +} + +void Material::Clear() { + ClearTextureMaps(); + + // Defaults correspond to the GLTF 2.0 spec. + name_.clear(); + color_factor_ = Vector4f(1.f, 1.f, 1.f, 1.f); + metallic_factor_ = 1.f; + roughness_factor_ = 1.f; + emissive_factor_ = Vector3f(0.f, 0.f, 0.f); + transparency_mode_ = TRANSPARENCY_OPAQUE; + alpha_cutoff_ = 0.5f; + double_sided_ = false; + normal_texture_scale_ = 1.0f; + + // Clear properties of material extensions to glTF 2.0 spec defaults. + unlit_ = false; + has_sheen_ = false; + sheen_color_factor_ = Vector3f(0.f, 0.f, 0.f); + sheen_roughness_factor_ = 0.f; + has_transmission_ = false; + transmission_factor_ = 0.f; + has_clearcoat_ = false; + clearcoat_factor_ = 0.f; + clearcoat_roughness_factor_ = 0.f; + has_volume_ = false; + thickness_factor_ = 0.f; + attenuation_distance_ = std::numeric_limits::max(); // Infinity. + attenuation_color_ = Vector3f(1.f, 1.f, 1.f); + has_ior_ = false; + ior_ = 1.5f; + has_specular_ = false; + specular_factor_ = 1.f; + specular_color_factor_ = Vector3f(1.f, 1.f, 1.f); +} + +void Material::ClearTextureMaps() { + texture_maps_.clear(); + texture_map_type_to_index_map_.clear(); +} + +void Material::SetTextureMap(TextureMap &&texture_map) { + std::unique_ptr new_texture_map(new TextureMap); + *new_texture_map = std::move(texture_map); + SetTextureMap(std::move(new_texture_map)); +} + +void Material::SetTextureMap(std::unique_ptr texture_map) { + const TextureMap::Type type = texture_map->type(); + const auto it = texture_map_type_to_index_map_.find(type); + // Only one texture of a given type is allowed to exist. + if (it == texture_map_type_to_index_map_.end()) { + texture_maps_.push_back(std::move(texture_map)); + texture_map_type_to_index_map_[type] = texture_maps_.size() - 1; + } else { + texture_maps_[it->second] = std::move(texture_map); + } +} + +void Material::SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, + int tex_coord_index) { + SetTextureMap(std::move(texture), texture_map_type, + TextureMap::WrappingMode(TextureMap::CLAMP_TO_EDGE), + tex_coord_index); +} + +void Material::SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + texture_map->SetProperties(texture_map_type, wrapping_mode, tex_coord_index); + + if (texture_library_) { + texture_map->SetTexture(texture.get()); + texture_library_->PushTexture(std::move(texture)); + } else { + texture_map->SetTexture(std::move(texture)); + } + SetTextureMap(std::move(texture_map)); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + int tex_coord_index) { + return SetTextureMap(texture, texture_map_type, + TextureMap::WrappingMode(TextureMap::CLAMP_TO_EDGE), + TextureMap::UNSPECIFIED, TextureMap::UNSPECIFIED, + tex_coord_index); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + return SetTextureMap(std::move(texture_map), texture, texture_map_type, + wrapping_mode, TextureMap::UNSPECIFIED, + TextureMap::UNSPECIFIED, tex_coord_index); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + return SetTextureMap(std::move(texture_map), texture, texture_map_type, + wrapping_mode, min_filter, mag_filter, tex_coord_index); +} + +Status Material::SetTextureMap(Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + const TextureTransform &transform, + int tex_coord_index) { + std::unique_ptr texture_map(new TextureMap); + texture_map->SetTransform(transform); + return SetTextureMap(std::move(texture_map), texture, texture_map_type, + wrapping_mode, min_filter, mag_filter, tex_coord_index); +} + +Status Material::SetTextureMap(std::unique_ptr texture_map, + Texture *texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + int tex_coord_index) { + if (!IsTextureOwned(*texture)) { + return Status(Status::DRACO_ERROR, + "Provided texture is not owned by the material."); + } + texture_map->SetProperties(texture_map_type, wrapping_mode, tex_coord_index, + min_filter, mag_filter); + texture_map->SetTexture(texture); + SetTextureMap(std::move(texture_map)); + return OkStatus(); +} + +bool Material::IsTextureOwned(const Texture &texture) { + if (texture_library_) { + // Ensure the texture is owned by the texture library. + for (int ti = 0; ti < texture_library_->NumTextures(); ++ti) { + if (texture_library_->GetTexture(ti) == &texture) { + return true; + } + } + return false; + } + // Else we need to check every texture map of this material. + for (int ti = 0; ti < NumTextureMaps(); ++ti) { + if (GetTextureMapByIndex(ti)->texture() == &texture) { + return true; + } + } + return false; +} + +std::unique_ptr Material::RemoveTextureMapByIndex(int index) { + if (index < 0 || index >= texture_maps_.size()) { + return nullptr; + } + std::unique_ptr ret = std::move(texture_maps_[index]); + texture_maps_.erase(texture_maps_.begin() + index); + // A texture map was removed and we need to update + // |texture_map_type_to_index_map_| to reflect the changes. + for (int i = index; i < texture_maps_.size(); ++i) { + texture_map_type_to_index_map_[texture_maps_[i]->type()] = i; + } + // Delete the removed texture map type. + texture_map_type_to_index_map_.erase( + texture_map_type_to_index_map_.find(ret->type())); + return ret; +} + +std::unique_ptr Material::RemoveTextureMapByType( + TextureMap::Type texture_type) { + const auto it = texture_map_type_to_index_map_.find(texture_type); + if (it == texture_map_type_to_index_map_.end()) { + return nullptr; + } + return RemoveTextureMapByIndex(it->second); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material.h b/contrib/draco/src/draco/material/material.h new file mode 100644 index 000000000..7c405b45c --- /dev/null +++ b/contrib/draco/src/draco/material/material.h @@ -0,0 +1,276 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_MATERIAL_MATERIAL_H_ +#define DRACO_MATERIAL_MATERIAL_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/status.h" +#include "draco/core/vector_d.h" +#include "draco/texture/texture_library.h" +#include "draco/texture/texture_map.h" + +namespace draco { + +// Material specification for Draco geometry. Parameters are based on the +// metallic-roughness PBR model adopted by GLTF 2.0 standard: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials +class Material { + public: + enum TransparencyMode { + TRANSPARENCY_OPAQUE = 0, + TRANSPARENCY_MASK, + TRANSPARENCY_BLEND + }; + + Material(); + explicit Material(TextureLibrary *texture_library); + + // Copies all material data from the |src| material to this material. + void Copy(const Material &src); + + // Deletes all texture maps and resets all material properties to default + // values. + void Clear(); + + // Deletes all texture maps from the material while keeping other material + // properties unchanged. + void ClearTextureMaps(); + + const std::string &GetName() const { return name_; } + void SetName(const std::string &name) { name_ = name; } + Vector4f GetColorFactor() const { return color_factor_; } + void SetColorFactor(const Vector4f &color_factor) { + color_factor_ = color_factor; + } + float GetMetallicFactor() const { return metallic_factor_; } + void SetMetallicFactor(float metallic_factor) { + metallic_factor_ = metallic_factor; + } + float GetRoughnessFactor() const { return roughness_factor_; } + void SetRoughnessFactor(float roughness_factor) { + roughness_factor_ = roughness_factor; + } + Vector3f GetEmissiveFactor() const { return emissive_factor_; } + void SetEmissiveFactor(const Vector3f &emissive_factor) { + emissive_factor_ = emissive_factor; + } + bool GetDoubleSided() const { return double_sided_; } + void SetDoubleSided(bool double_sided) { double_sided_ = double_sided; } + TransparencyMode GetTransparencyMode() const { return transparency_mode_; } + void SetTransparencyMode(TransparencyMode mode) { transparency_mode_ = mode; } + float GetAlphaCutoff() const { return alpha_cutoff_; } + void SetAlphaCutoff(float alpha_cutoff) { alpha_cutoff_ = alpha_cutoff; } + float GetNormalTextureScale() const { return normal_texture_scale_; } + void SetNormalTextureScale(float scale) { normal_texture_scale_ = scale; } + + // Properties of glTF material extension KHR_materials_unlit. + bool GetUnlit() const { return unlit_; } + void SetUnlit(bool unlit) { unlit_ = unlit; } + + // Properties of glTF material extension KHR_materials_sheen. + bool HasSheen() const { return has_sheen_; } + void SetHasSheen(bool value) { has_sheen_ = value; } + Vector3f GetSheenColorFactor() const { return sheen_color_factor_; } + void SetSheenColorFactor(const Vector3f &value) { + sheen_color_factor_ = value; + } + float GetSheenRoughnessFactor() const { return sheen_roughness_factor_; } + void SetSheenRoughnessFactor(float value) { sheen_roughness_factor_ = value; } + + // Properties of glTF material extension KHR_materials_transmission. + bool HasTransmission() const { return has_transmission_; } + void SetHasTransmission(bool value) { has_transmission_ = value; } + float GetTransmissionFactor() const { return transmission_factor_; } + void SetTransmissionFactor(float value) { transmission_factor_ = value; } + + // Properties of glTF material extension KHR_materials_clearcoat. + bool HasClearcoat() const { return has_clearcoat_; } + void SetHasClearcoat(bool value) { has_clearcoat_ = value; } + float GetClearcoatFactor() const { return clearcoat_factor_; } + void SetClearcoatFactor(float value) { clearcoat_factor_ = value; } + float GetClearcoatRoughnessFactor() const { + return clearcoat_roughness_factor_; + } + void SetClearcoatRoughnessFactor(float value) { + clearcoat_roughness_factor_ = value; + } + + // Properties of glTF material extension KHR_materials_volume. + bool HasVolume() const { return has_volume_; } + void SetHasVolume(bool value) { has_volume_ = value; } + float GetThicknessFactor() const { return thickness_factor_; } + void SetThicknessFactor(float value) { thickness_factor_ = value; } + float GetAttenuationDistance() const { return attenuation_distance_; } + void SetAttenuationDistance(float value) { attenuation_distance_ = value; } + Vector3f GetAttenuationColor() const { return attenuation_color_; } + void SetAttenuationColor(const Vector3f &value) { + attenuation_color_ = value; + } + + // Properties of glTF material extension KHR_materials_ior. + bool HasIor() const { return has_ior_; } + void SetHasIor(bool value) { has_ior_ = value; } + float GetIor() const { return ior_; } + void SetIor(float value) { ior_ = value; } + + // Properties of glTF material extension KHR_materials_specular. + bool HasSpecular() const { return has_specular_; } + void SetHasSpecular(bool value) { has_specular_ = value; } + float GetSpecularFactor() const { return specular_factor_; } + void SetSpecularFactor(float value) { specular_factor_ = value; } + Vector3f GetSpecularColorFactor() const { return specular_color_factor_; } + void SetSpecularColorFactor(const Vector3f &value) { + specular_color_factor_ = value; + } + + // Methods for working with texture maps. + size_t NumTextureMaps() const { return texture_maps_.size(); } + const TextureMap *GetTextureMapByIndex(int index) const { + return texture_maps_[index].get(); + } + TextureMap *GetTextureMapByIndex(int index) { + return texture_maps_[index].get(); + } + const TextureMap *GetTextureMapByType(TextureMap::Type texture_type) const { + const auto it = texture_map_type_to_index_map_.find(texture_type); + if (it == texture_map_type_to_index_map_.end()) { + return nullptr; + } + return GetTextureMapByIndex(it->second); + } + TextureMap *GetTextureMapByType(TextureMap::Type texture_type) { + const auto it = texture_map_type_to_index_map_.find(texture_type); + if (it == texture_map_type_to_index_map_.end()) { + return nullptr; + } + return GetTextureMapByIndex(it->second); + } + + // TODO(b/146061359): Refactor the set texture map code. + // Specifies a new texture map using a texture with a given type. + // |tex_coord_index| defines which texture coordinate attribute should be used + // to map the texture on the underlying geometry (e.g. tex_coord_index 0 would + // use the first texture coordinate attribute). + void SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, int tex_coord_index); + void SetTextureMap(std::unique_ptr texture, + TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index); + + // Sets a new texture map using a |texture| that is already owned by this + // material (that is by one of its texture maps or by the unerlying + // |texture_library_|). |transform| is the texture map's transform if set. + // |min_filter| and |mag_filter| are the texture filter types. Returns error + // status if provided |texture| is not owned by the material. + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + int tex_coord_index); + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + int tex_coord_index); + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, int tex_coord_index); + Status SetTextureMap(Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, + const TextureTransform &transform, int tex_coord_index); + + // Removes a texture map from the material based on its index or texture type. + // The material releases the ownership of the texture map and returns it as + // a unique_ptr to allow the caller to use the texture map for other purposes. + std::unique_ptr RemoveTextureMapByIndex(int index); + std::unique_ptr RemoveTextureMapByType( + TextureMap::Type texture_type); + + private: + void SetTextureMap(TextureMap &&texture_map); + void SetTextureMap(std::unique_ptr texture_map); + Status SetTextureMap(std::unique_ptr texture_map, + Texture *texture, TextureMap::Type texture_map_type, + TextureMap::WrappingMode wrapping_mode, + TextureMap::FilterType min_filter, + TextureMap::FilterType mag_filter, int tex_coord_index); + + // Returns true if the |texture| is owned by the material. + bool IsTextureOwned(const Texture &texture); + + private: + std::string name_; + Vector4f color_factor_; + float metallic_factor_; + float roughness_factor_; + Vector3f emissive_factor_; + bool double_sided_; + TransparencyMode transparency_mode_; + float alpha_cutoff_; + float normal_texture_scale_; + + // Properties of glTF material extension KHR_materials_unlit. + bool unlit_; + + // Properties of glTF material extension KHR_materials_sheen. + bool has_sheen_; + Vector3f sheen_color_factor_; + float sheen_roughness_factor_; + + // Properties of glTF material extension KHR_materials_transmission. + bool has_transmission_; + float transmission_factor_; + + // Properties of glTF material extension KHR_materials_clearcoat. + bool has_clearcoat_; + float clearcoat_factor_; + float clearcoat_roughness_factor_; + + // Properties of glTF material extension KHR_materials_volume. + bool has_volume_; + float thickness_factor_; + float attenuation_distance_; + Vector3f attenuation_color_; + + // Properties of glTF material extension KHR_materials_ior. + bool has_ior_; + float ior_; + + // Properties of glTF material extension KHR_materials_specular. + bool has_specular_; + float specular_factor_; + Vector3f specular_color_factor_; + + // Texture maps. + std::vector> texture_maps_; + + // Map between a texture type to texture index in |texture_maps_|. Allows fast + // retrieval of texture maps based on their type. + std::unordered_map texture_map_type_to_index_map_; + + // Optional pointer to a library that holds ownership of textures used for + // this material. If set to nullptr, the texture ownership will be assigned + // to the newly created TextureMaps directly. + TextureLibrary *texture_library_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MATERIAL_MATERIAL_H_ diff --git a/contrib/draco/src/draco/material/material_library.cc b/contrib/draco/src/draco/material/material_library.cc new file mode 100644 index 000000000..f2165295f --- /dev/null +++ b/contrib/draco/src/draco/material/material_library.cc @@ -0,0 +1,125 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/material/material_library.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +namespace draco { + +void MaterialLibrary::Copy(const MaterialLibrary &src) { + Clear(); + Append(src); +} + +void MaterialLibrary::Append(const MaterialLibrary &src) { + const size_t old_num_materials = materials_.size(); + materials_.resize(old_num_materials + src.materials_.size()); + for (int i = 0; i < src.materials_.size(); ++i) { + materials_[old_num_materials + i] = + std::unique_ptr(new Material(&texture_library_)); + materials_[old_num_materials + i]->Copy(*src.materials_[i]); + } + + const size_t old_num_textures = texture_library_.NumTextures(); + texture_library_.Append(src.texture_library_); + for (int i = 0; i < src.materials_variants_names_.size(); i++) { + materials_variants_names_.push_back(src.materials_variants_names_[i]); + } + + // Remap all texture maps to the textures in the new texture library. + + // First gather mapping between texture maps and textures in the old material + // library. + const auto texture_map_to_index = + ComputeTextureMapToTextureIndexMapping(src.texture_library_); + + // Remap all texture maps to textures stored in the new texture library. + for (auto it = texture_map_to_index.begin(); it != texture_map_to_index.end(); + ++it) { + TextureMap *const texture_map = it->first; + const int texture_index = old_num_textures + it->second; + texture_map->SetTexture(texture_library_.GetTexture(texture_index)); + } +} + +std::unique_ptr MaterialLibrary::RemoveMaterial(int index) { + std::unique_ptr ret = std::move(materials_[index]); + materials_.erase(materials_.begin() + index); + return ret; +} + +void MaterialLibrary::RemoveUnusedTextures() { + const auto texture_map_to_index = + ComputeTextureMapToTextureIndexMapping(texture_library_); + + // Mark which textures are used. + std::vector is_texture_used(texture_library_.NumTextures(), false); + for (auto it = texture_map_to_index.begin(); it != texture_map_to_index.end(); + ++it) { + is_texture_used[it->second] = true; + } + + // Remove all textures that are not used (from backwards to avoid updating + // entries in the |is_texture_used| vector). + for (int i = texture_library_.NumTextures() - 1; i >= 0; --i) { + if (!is_texture_used[i]) { + texture_library_.RemoveTexture(i); + } + } +} + +std::map +MaterialLibrary::ComputeTextureMapToTextureIndexMapping( + const TextureLibrary &library) const { + std::map map_to_index; + for (int mi = 0; mi < materials_.size(); ++mi) { + for (int ti = 0; ti < materials_[mi]->NumTextureMaps(); ++ti) { + TextureMap *const texture_map = materials_[mi]->GetTextureMapByIndex(ti); + for (int tli = 0; tli < library.NumTextures(); ++tli) { + if (library.GetTexture(tli) != texture_map->texture()) { + continue; + } + map_to_index[texture_map] = tli; + break; + } + } + } + return map_to_index; +} + +void MaterialLibrary::Clear() { + materials_.clear(); + texture_library_.Clear(); + materials_variants_names_.clear(); +} + +Material *MaterialLibrary::MutableMaterial(int index) { + if (index < 0) { + return nullptr; + } + if (materials_.size() <= index) { + const int old_size = materials_.size(); + materials_.resize(index + 1); + // Ensure all newly created materials are valid. + for (int i = old_size; i < index + 1; ++i) { + materials_[i] = + std::unique_ptr(new Material(&texture_library_)); + } + } + return materials_[index].get(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material_library.h b/contrib/draco/src/draco/material/material_library.h new file mode 100644 index 000000000..574d86b23 --- /dev/null +++ b/contrib/draco/src/draco/material/material_library.h @@ -0,0 +1,104 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_MATERIAL_MATERIAL_LIBRARY_H_ +#define DRACO_MATERIAL_MATERIAL_LIBRARY_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/material/material.h" +#include "draco/texture/texture_library.h" + +namespace draco { + +// MaterialLibrary holds an array of materials that are applied to a single +// model. +class MaterialLibrary { + public: + MaterialLibrary() = default; + + // Copies the |src| into this instance. + void Copy(const MaterialLibrary &src); + + // Appends materials from the |src| library to this library. All materials + // and textures are copied over. + void Append(const MaterialLibrary &src); + + // Deletes all materials from the material library. + void Clear(); + + // The number of materials stored in the library. All materials are stored + // with indices <0, num_materials() - 1>. + size_t NumMaterials() const { return materials_.size(); } + + // Returns a material with a given index or nullptr if the index is not valid. + const Material *GetMaterial(int index) const { + if (index < 0 || index >= materials_.size()) { + return nullptr; + } + return materials_[index].get(); + } + + // Returns a mutable pointer to a given material. If the material with the + // specified |index| does not exist, it is automatically created. + Material *MutableMaterial(int index); + + // Removes a material with a given index and returns it. Caller can ignore the + // returned value, in which case the material will be automatically deleted. + // Index of all subsequent materials will be decremented by one. + std::unique_ptr RemoveMaterial(int index); + + const TextureLibrary &GetTextureLibrary() const { return texture_library_; } + TextureLibrary &MutableTextureLibrary() { return texture_library_; } + + // Removes all textures that are not referenced by a TextureMap from the + // texture library. + void RemoveUnusedTextures(); + + // Returns a map between each TextureMap object and associated texture index + // in the texture |library|. + std::map ComputeTextureMapToTextureIndexMapping( + const TextureLibrary &library) const; + + // Creates a named materials variant and returns its index. + int AddMaterialsVariant(const std::string &name) { + materials_variants_names_.push_back(name); + return materials_variants_names_.size() - 1; + } + + // Returns the number of materials variants. + int NumMaterialsVariants() const { return materials_variants_names_.size(); } + + // Returns the name of a materials variant. + const std::string &GetMaterialsVariantName(int index) const { + return materials_variants_names_[index]; + } + + private: + std::vector> materials_; + std::vector materials_variants_names_; + + // Container for storing all textures used by materials of this library. + TextureLibrary texture_library_; +}; + +} // namespace draco + +#endif // DRACO_MATERIAL_MATERIAL_LIBRARY_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material_library_test.cc b/contrib/draco/src/draco/material/material_library_test.cc new file mode 100644 index 000000000..a110fa4db --- /dev/null +++ b/contrib/draco/src/draco/material/material_library_test.cc @@ -0,0 +1,155 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/material/material_library.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST(MaterialLibraryTest, TestMaterials) { + // Test verifies that we can modify materials in a library. + draco::MaterialLibrary library; + ASSERT_EQ(library.NumMaterials(), 0); + + // Add a new material to the library. + const draco::Material *const new_mat = library.MutableMaterial(0); + ASSERT_NE(new_mat, nullptr); + ASSERT_EQ(library.NumMaterials(), 1); + + const draco::Material *const new_mat2 = library.MutableMaterial(2); + ASSERT_NE(new_mat2, nullptr); + ASSERT_EQ(library.NumMaterials(), 3); + ASSERT_EQ(library.GetMaterial(2), new_mat2); + + // Ensure that even though we call mutable_material multiple times, it does + // not increase the number of materials associated to the library. + for (int i = 0; i < library.NumMaterials(); ++i) { + ASSERT_NE(library.MutableMaterial(i), nullptr); + } + ASSERT_EQ(library.NumMaterials(), 3); + + // Check that material variants can be added and cleared. + library.AddMaterialsVariant("Milk Truck"); + library.AddMaterialsVariant("Ice Cream Truck"); + ASSERT_EQ(library.NumMaterialsVariants(), 2); + ASSERT_EQ(library.GetMaterialsVariantName(0), "Milk Truck"); + ASSERT_EQ(library.GetMaterialsVariantName(1), "Ice Cream Truck"); + + library.Clear(); + ASSERT_EQ(library.NumMaterials(), 0); + ASSERT_EQ(library.NumMaterialsVariants(), 0); +} + +TEST(MaterialLibraryTest, TestMaterialsCopy) { + // Test verifies that we can copy a material library. + draco::MaterialLibrary library; + library.MutableMaterial(0)->SetMetallicFactor(2.4f); + library.MutableMaterial(3)->SetRoughnessFactor(1.2f); + library.AddMaterialsVariant("Milk Truck"); + library.AddMaterialsVariant("Ice Cream Truck"); + + draco::MaterialLibrary new_library; + new_library.Copy(library); + ASSERT_EQ(library.NumMaterials(), new_library.NumMaterials()); + ASSERT_EQ(library.GetMaterial(0)->GetMetallicFactor(), + new_library.GetMaterial(0)->GetMetallicFactor()); + ASSERT_EQ(library.GetMaterial(3)->GetRoughnessFactor(), + new_library.GetMaterial(3)->GetRoughnessFactor()); + ASSERT_EQ(new_library.NumMaterialsVariants(), 2); + ASSERT_EQ(new_library.GetMaterialsVariantName(0), "Milk Truck"); + ASSERT_EQ(new_library.GetMaterialsVariantName(1), "Ice Cream Truck"); +} + +TEST(MaterialLibraryTest, TestTextureLibrary) { + // Tests that texture library is properly updated when we add new textures + // to a material belonging to the material library. + std::unique_ptr texture_0(new draco::Texture()); + std::unique_ptr texture_1(new draco::Texture()); + + draco::MaterialLibrary library; + library.MutableMaterial(0)->SetTextureMap(std::move(texture_0), + draco::TextureMap::COLOR, 0); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 1); + library.MutableMaterial(3)->SetTextureMap(std::move(texture_1), + draco::TextureMap::COLOR, 0); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 2); +} + +TEST(MaterialLibraryTest, RemoveUnusedTextures) { + // Test verifies that we can remove unusued textures from the material + // library. + draco::MaterialLibrary library; + + // Create dummy textures. + std::unique_ptr texture_0(new draco::Texture()); + std::unique_ptr texture_1(new draco::Texture()); + std::unique_ptr texture_2(new draco::Texture()); + + // Add them to the materials of the library. + library.MutableMaterial(0)->SetTextureMap(std::move(texture_0), + draco::TextureMap::COLOR, 0); + library.MutableMaterial(0)->SetTextureMap( + std::move(texture_1), draco::TextureMap::METALLIC_ROUGHNESS, 0); + library.MutableMaterial(1)->SetTextureMap(std::move(texture_2), + draco::TextureMap::COLOR, 0); + + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 3); + + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 3); + + // Remove texture map from a material. + library.MutableMaterial(0)->RemoveTextureMapByType( + draco::TextureMap::METALLIC_ROUGHNESS); + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 2); + + library.MutableMaterial(1)->RemoveTextureMapByType(draco::TextureMap::COLOR); + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 1); + + library.MutableMaterial(0)->RemoveTextureMapByType(draco::TextureMap::COLOR); + library.RemoveUnusedTextures(); + ASSERT_EQ(library.GetTextureLibrary().NumTextures(), 0); +} + +TEST(MaterialLibraryTest, RemoveMaterial) { + // Tests that we can safely remove materials from the material library. + draco::MaterialLibrary library; + library.MutableMaterial(0)->SetMetallicFactor(0.f); + library.MutableMaterial(1)->SetMetallicFactor(1.f); + library.MutableMaterial(2)->SetMetallicFactor(2.f); + library.MutableMaterial(3)->SetMetallicFactor(3.f); + + ASSERT_EQ(library.NumMaterials(), 4); + + ASSERT_EQ(library.RemoveMaterial(0)->GetMetallicFactor(), 0.f); + ASSERT_EQ(library.NumMaterials(), 3); + + ASSERT_EQ(library.RemoveMaterial(1)->GetMetallicFactor(), 2.f); + ASSERT_EQ(library.NumMaterials(), 2); + + ASSERT_EQ(library.RemoveMaterial(1)->GetMetallicFactor(), 3.f); + ASSERT_EQ(library.NumMaterials(), 1); + + ASSERT_EQ(library.RemoveMaterial(0)->GetMetallicFactor(), 1.f); + ASSERT_EQ(library.NumMaterials(), 0); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/material/material_test.cc b/contrib/draco/src/draco/material/material_test.cc new file mode 100644 index 000000000..8c999a532 --- /dev/null +++ b/contrib/draco/src/draco/material/material_test.cc @@ -0,0 +1,320 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/material/material.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" + +namespace { + +TEST(MaterialTest, TestMaterialAccess) { + // Tests that we can set and get material properties. + draco::Material material; + + material.SetName("Superalloy"); + ASSERT_EQ(material.GetName(), "Superalloy"); + material.SetColorFactor(draco::Vector4f(1.f, 0.2f, 0.1f, 0.9f)); + ASSERT_EQ(material.GetColorFactor(), draco::Vector4f(1.f, 0.2f, 0.1f, 0.9f)); + material.SetMetallicFactor(0.3f); + ASSERT_EQ(material.GetMetallicFactor(), 0.3f); + material.SetRoughnessFactor(0.2f); + ASSERT_EQ(material.GetRoughnessFactor(), 0.2f); + material.SetEmissiveFactor(draco::Vector3f(0.2f, 0.f, 0.1f)); + ASSERT_EQ(material.GetEmissiveFactor(), draco::Vector3f(0.2f, 0.f, 0.1f)); + + // Set and check the properties of material extensions. + material.SetUnlit(true); + ASSERT_TRUE(material.GetUnlit()); + material.SetHasSheen(true); + ASSERT_TRUE(material.HasSheen()); + material.SetSheenColorFactor(draco::Vector3f(0.4f, 0.2f, 0.8f)); + ASSERT_EQ(material.GetSheenColorFactor(), draco::Vector3f(0.4f, 0.2f, 0.8f)); + material.SetSheenRoughnessFactor(0.428f); + ASSERT_EQ(material.GetSheenRoughnessFactor(), 0.428f); + material.SetHasTransmission(true); + ASSERT_TRUE(material.HasTransmission()); + material.SetTransmissionFactor(0.5f); + ASSERT_EQ(material.GetTransmissionFactor(), 0.5f); + material.SetHasClearcoat(true); + ASSERT_TRUE(material.HasClearcoat()); + material.SetClearcoatFactor(0.6f); + ASSERT_EQ(material.GetClearcoatFactor(), 0.6f); + material.SetClearcoatRoughnessFactor(0.7f); + ASSERT_EQ(material.GetClearcoatRoughnessFactor(), 0.7f); + material.SetHasVolume(true); + ASSERT_TRUE(material.HasVolume()); + material.SetThicknessFactor(0.8f); + ASSERT_EQ(material.GetThicknessFactor(), 0.8f); + material.SetAttenuationDistance(0.9f); + ASSERT_EQ(material.GetAttenuationDistance(), 0.9f); + material.SetAttenuationColor(draco::Vector3f(0.2f, 0.5f, 0.8f)); + ASSERT_EQ(material.GetAttenuationColor(), draco::Vector3f(0.2f, 0.5f, 0.8f)); + material.SetHasIor(true); + ASSERT_TRUE(material.HasIor()); + material.SetIor(1.1f); + ASSERT_EQ(material.GetIor(), 1.1f); + material.SetHasSpecular(true); + ASSERT_TRUE(material.HasSpecular()); + material.SetSpecularFactor(0.01f); + ASSERT_EQ(material.GetSpecularFactor(), 0.01f); + material.SetSpecularColorFactor(draco::Vector3f(0.4f, 1.f, 1.f)); + ASSERT_EQ(material.GetSpecularColorFactor(), draco::Vector3f(0.4f, 1.f, 1.f)); + + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + ASSERT_EQ(material.NumTextureMaps(), 0); + + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + + material.SetTextureMap(std::move(texture), draco::TextureMap::COLOR, 0); + + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + ASSERT_EQ(material.NumTextureMaps(), 1); + ASSERT_EQ(material.GetTextureMapByIndex(0), + material.GetTextureMapByType(draco::TextureMap::COLOR)); + + std::unique_ptr texture2 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture2, nullptr); + material.SetTextureMap(std::move(texture2), draco::TextureMap::EMISSIVE, 1); + + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::EMISSIVE), nullptr); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::EMISSIVE) + ->tex_coord_index(), + 1); + ASSERT_EQ(material.NumTextureMaps(), 2); + + // Try to add the emissive texture one more time. This should replace the + // previous instance of the emissive texture on the material. + std::unique_ptr texture3 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture3, nullptr); + material.SetTextureMap(std::move(texture2), draco::TextureMap::EMISSIVE, 2); + ASSERT_EQ(material.NumTextureMaps(), 2); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::EMISSIVE) + ->tex_coord_index(), + 2); + + std::unique_ptr texture_map4(new draco::TextureMap()); + std::unique_ptr texture4 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + material.SetTextureMap(std::move(texture4), draco::TextureMap::ROUGHNESS, 0); + ASSERT_EQ(material.NumTextureMaps(), 3); + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::ROUGHNESS), + nullptr); + + material.SetTransparencyMode(draco::Material::TRANSPARENCY_BLEND); + ASSERT_EQ(material.GetTransparencyMode(), + draco::Material::TRANSPARENCY_BLEND); + material.SetAlphaCutoff(0.2f); + ASSERT_EQ(material.GetAlphaCutoff(), 0.2f); + material.SetNormalTextureScale(0.75f); + ASSERT_EQ(material.GetNormalTextureScale(), 0.75f); + + material.ClearTextureMaps(); + ASSERT_EQ(material.NumTextureMaps(), 0); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + + // Metallic factor should be unchanged. + ASSERT_EQ(material.GetMetallicFactor(), 0.3f); + + material.Clear(); + // Metallic factor should be reset to default. + ASSERT_NE(material.GetMetallicFactor(), 0.3f); + + ASSERT_EQ(material.GetDoubleSided(), false); + material.SetDoubleSided(true); + ASSERT_EQ(material.GetDoubleSided(), true); +} + +TEST(MaterialTest, TestMaterialCopy) { + draco::Material material; + material.SetName("Antimatter"); + material.SetColorFactor(draco::Vector4f(0.3f, 0.2f, 0.4f, 0.9f)); + material.SetMetallicFactor(0.2f); + material.SetRoughnessFactor(0.4f); + material.SetEmissiveFactor(draco::Vector3f(0.3f, 0.1f, 0.2f)); + material.SetTransparencyMode(draco::Material::TRANSPARENCY_MASK); + material.SetAlphaCutoff(0.25f); + material.SetDoubleSided(true); + material.SetNormalTextureScale(0.75f); + + // Set the properties of material extensions. + material.SetUnlit(true); + material.SetHasSheen(true); + material.SetSheenColorFactor(draco::Vector3f(0.4f, 0.2f, 0.8f)); + material.SetSheenRoughnessFactor(0.428f); + material.SetHasTransmission(true); + material.SetTransmissionFactor(0.5f); + material.SetHasClearcoat(true); + material.SetClearcoatFactor(0.6f); + material.SetClearcoatRoughnessFactor(0.7f); + material.SetHasVolume(true); + material.SetThicknessFactor(0.8f); + material.SetAttenuationDistance(0.9f); + material.SetAttenuationColor(draco::Vector3f(0.2f, 0.5f, 0.8f)); + material.SetHasIor(true); + material.SetIor(1.1f); + material.SetHasSpecular(true); + material.SetSpecularFactor(0.01f); + material.SetSpecularColorFactor(draco::Vector3f(0.4f, 1.f, 1.f)); + + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + material.SetTextureMap(std::move(texture), draco::TextureMap::EMISSIVE, 2); + + draco::Material new_material; + new_material.Copy(material); + + ASSERT_EQ(material.GetName(), new_material.GetName()); + ASSERT_EQ(material.GetColorFactor(), new_material.GetColorFactor()); + ASSERT_EQ(material.GetMetallicFactor(), new_material.GetMetallicFactor()); + ASSERT_EQ(material.GetRoughnessFactor(), new_material.GetRoughnessFactor()); + ASSERT_EQ(material.GetEmissiveFactor(), new_material.GetEmissiveFactor()); + ASSERT_EQ(material.GetTransparencyMode(), new_material.GetTransparencyMode()); + ASSERT_EQ(material.GetAlphaCutoff(), new_material.GetAlphaCutoff()); + ASSERT_EQ(material.GetDoubleSided(), new_material.GetDoubleSided()); + ASSERT_EQ(material.GetNormalTextureScale(), + new_material.GetNormalTextureScale()); + + // Check that the properties of material extensions have been copied. + ASSERT_EQ(material.GetUnlit(), new_material.GetUnlit()); + ASSERT_EQ(material.HasSheen(), new_material.HasSheen()); + ASSERT_EQ(material.GetSheenColorFactor(), new_material.GetSheenColorFactor()); + ASSERT_EQ(material.GetSheenRoughnessFactor(), + new_material.GetSheenRoughnessFactor()); + ASSERT_TRUE(material.HasTransmission()); + ASSERT_EQ(material.GetTransmissionFactor(), + new_material.GetTransmissionFactor()); + ASSERT_TRUE(material.HasClearcoat()); + ASSERT_EQ(material.GetClearcoatFactor(), new_material.GetClearcoatFactor()); + ASSERT_EQ(material.GetClearcoatRoughnessFactor(), + new_material.GetClearcoatRoughnessFactor()); + ASSERT_TRUE(material.HasVolume()); + ASSERT_EQ(material.GetThicknessFactor(), new_material.GetThicknessFactor()); + ASSERT_EQ(material.GetAttenuationDistance(), + new_material.GetAttenuationDistance()); + ASSERT_EQ(material.GetAttenuationColor(), new_material.GetAttenuationColor()); + ASSERT_TRUE(material.HasIor()); + ASSERT_EQ(material.GetIor(), new_material.GetIor()); + ASSERT_TRUE(material.HasSpecular()); + ASSERT_EQ(material.GetSpecularFactor(), new_material.GetSpecularFactor()); + ASSERT_EQ(material.GetSpecularColorFactor(), + new_material.GetSpecularColorFactor()); + + for (int i = 0; i < draco::TextureMap::TEXTURE_TYPES_COUNT; ++i) { + const draco::TextureMap::Type texture_map_type = + static_cast(i); + if (material.GetTextureMapByType(texture_map_type) == nullptr) { + ASSERT_EQ(new_material.GetTextureMapByType(texture_map_type), nullptr); + continue; + } + if (material.GetTextureMapByType(texture_map_type)->texture() == nullptr) { + ASSERT_EQ(new_material.GetTextureMapByType(texture_map_type)->texture(), + nullptr); + } else { + ASSERT_NE(new_material.GetTextureMapByType(texture_map_type)->texture(), + nullptr); + ASSERT_EQ( + material.GetTextureMapByType(texture_map_type)->tex_coord_index(), + new_material.GetTextureMapByType(texture_map_type) + ->tex_coord_index()); + } + } + + ASSERT_EQ(material.NumTextureMaps(), new_material.NumTextureMaps()); + for (int i = 0; i < material.NumTextureMaps(); ++i) { + const draco::TextureMap *const tm0 = material.GetTextureMapByIndex(i); + const draco::TextureMap *const tm1 = new_material.GetTextureMapByIndex(i); + ASSERT_NE(tm0, nullptr); + ASSERT_NE(tm1, nullptr); + ASSERT_EQ(tm0->type(), tm1->type()); + } +} + +TEST(MaterialTest, RemoveTextureMap) { + // Tests that we can remove existing texture maps from a material. + draco::Material material; + + // Add some dummy textures to the material. + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + material.SetTextureMap(std::move(texture), draco::TextureMap::COLOR, 0); + + std::unique_ptr texture_2 = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + + material.SetTextureMap(std::move(texture), draco::TextureMap::EMISSIVE, 0); + + ASSERT_EQ(material.NumTextureMaps(), 2); + + // Try to delete the color texture. + std::unique_ptr removed_texture = + material.RemoveTextureMapByType(draco::TextureMap::COLOR); + ASSERT_NE(removed_texture, nullptr); + ASSERT_EQ(removed_texture->type(), draco::TextureMap::COLOR); + ASSERT_EQ(material.NumTextureMaps(), 1); + ASSERT_NE(material.GetTextureMapByType(draco::TextureMap::EMISSIVE), nullptr); + ASSERT_EQ(material.GetTextureMapByIndex(0)->type(), + draco::TextureMap::EMISSIVE); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::COLOR), nullptr); + + removed_texture = material.RemoveTextureMapByIndex(0); + ASSERT_NE(removed_texture, nullptr); + ASSERT_EQ(removed_texture->type(), draco::TextureMap::EMISSIVE); + ASSERT_EQ(material.NumTextureMaps(), 0); + ASSERT_EQ(material.GetTextureMapByType(draco::TextureMap::EMISSIVE), nullptr); +} + +TEST(MaterialTest, SharedTexture) { + // Tests adding shared textures. + draco::Material material; + + // Add some dummy textures to the material. + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + ASSERT_NE(texture, nullptr); + draco::Texture *texture_raw = texture.get(); + material.SetTextureMap(std::move(texture), draco::TextureMap::COLOR, 0); + + DRACO_ASSERT_OK( + material.SetTextureMap(texture_raw, draco::TextureMap::EMISSIVE, 0)); + + ASSERT_EQ(material.NumTextureMaps(), 2); + + // Read a new texture. + texture = draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png")) + .value(); + // Texture is not owned by the material so we expect a failure. + ASSERT_FALSE( + material + .SetTextureMap(texture.get(), draco::TextureMap::AMBIENT_OCCLUSION, 0) + .ok()); +} + +} // namespace +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/material/material_utils.cc b/contrib/draco/src/draco/material/material_utils.cc new file mode 100644 index 000000000..7f9fcb621 --- /dev/null +++ b/contrib/draco/src/draco/material/material_utils.cc @@ -0,0 +1,14 @@ +// Copyright 2020 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// diff --git a/contrib/draco/src/draco/material/material_utils.h b/contrib/draco/src/draco/material/material_utils.h new file mode 100644 index 000000000..7f9fcb621 --- /dev/null +++ b/contrib/draco/src/draco/material/material_utils.h @@ -0,0 +1,14 @@ +// Copyright 2020 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// diff --git a/contrib/draco/src/draco/material/material_utils_test.cc b/contrib/draco/src/draco/material/material_utils_test.cc new file mode 100644 index 000000000..82a1227a2 --- /dev/null +++ b/contrib/draco/src/draco/material/material_utils_test.cc @@ -0,0 +1,24 @@ +// Copyright 2020 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/material/material_utils.h" + +#include +#include +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" + +namespace {} // namespace diff --git a/contrib/draco/src/draco/mesh/corner_table.cc b/contrib/draco/src/draco/mesh/corner_table.cc index 3f92f651a..6494c1572 100644 --- a/contrib/draco/src/draco/mesh/corner_table.cc +++ b/contrib/draco/src/draco/mesh/corner_table.cc @@ -15,6 +15,7 @@ #include "draco/mesh/corner_table.h" #include +#include #include "draco/attributes/geometry_indices.h" #include "draco/mesh/corner_table_iterators.h" diff --git a/contrib/draco/src/draco/mesh/corner_table.h b/contrib/draco/src/draco/mesh/corner_table.h index 3aa720fde..3088931c1 100644 --- a/contrib/draco/src/draco/mesh/corner_table.h +++ b/contrib/draco/src/draco/mesh/corner_table.h @@ -21,6 +21,7 @@ #include "draco/attributes/geometry_indices.h" #include "draco/core/draco_index_type_vector.h" #include "draco/core/macros.h" +#include "draco/draco_features.h" #include "draco/mesh/valence_cache.h" namespace draco { diff --git a/contrib/draco/src/draco/mesh/corner_table_iterators.h b/contrib/draco/src/draco/mesh/corner_table_iterators.h index 7122aa1be..72c70ac32 100644 --- a/contrib/draco/src/draco/mesh/corner_table_iterators.h +++ b/contrib/draco/src/draco/mesh/corner_table_iterators.h @@ -15,15 +15,23 @@ #ifndef DRACO_MESH_CORNER_TABLE_ITERATORS_H_ #define DRACO_MESH_CORNER_TABLE_ITERATORS_H_ +#include + #include "draco/mesh/corner_table.h" namespace draco { // Class for iterating over vertices in a 1-ring around the specified vertex. template -class VertexRingIterator - : public std::iterator { +class VertexRingIterator { public: + // Iterator traits expected by std libraries. + using iterator_category = std::forward_iterator_tag; + using value_type = VertexIndex; + using difference_type = std::ptrdiff_t; + using pointer = VertexIndex *; + using reference = VertexIndex &; + // std::iterator interface requires a default constructor. VertexRingIterator() : corner_table_(nullptr), @@ -111,9 +119,15 @@ class VertexRingIterator // Class for iterating over faces adjacent to the specified input face. template -class FaceAdjacencyIterator - : public std::iterator { +class FaceAdjacencyIterator { public: + // Iterator traits expected by std libraries. + using iterator_category = std::forward_iterator_tag; + using value_type = FaceIndex; + using difference_type = std::ptrdiff_t; + using pointer = FaceIndex *; + using reference = FaceIndex &; + // std::iterator interface requires a default constructor. FaceAdjacencyIterator() : corner_table_(nullptr), @@ -193,9 +207,15 @@ class FaceAdjacencyIterator // Class for iterating over corners attached to a specified vertex. template -class VertexCornersIterator - : public std::iterator { +class VertexCornersIterator { public: + // Iterator traits expected by std libraries. + using iterator_category = std::forward_iterator_tag; + using value_type = CornerIndex; + using difference_type = std::ptrdiff_t; + using pointer = CornerIndex *; + using reference = CornerIndex &; + // std::iterator interface requires a default constructor. VertexCornersIterator() : corner_table_(nullptr), diff --git a/contrib/draco/src/draco/mesh/corner_table_test.cc b/contrib/draco/src/draco/mesh/corner_table_test.cc new file mode 100644 index 000000000..f88d3ec96 --- /dev/null +++ b/contrib/draco/src/draco/mesh/corner_table_test.cc @@ -0,0 +1,126 @@ +#include "draco/mesh/corner_table.h" + +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/obj_decoder.h" +#include "draco/mesh/mesh_connected_components.h" +#include "draco/mesh/mesh_misc_functions.h" + +namespace draco { + +class CornerTableTest : public ::testing::Test { + protected: + std::unique_ptr DecodeObj(const std::string &file_name) const { + const std::string path = GetTestFileFullPath(file_name); + ObjDecoder decoder; + std::unique_ptr mesh(new Mesh()); + if (!decoder.DecodeFromFile(path, mesh.get()).ok()) { + return nullptr; + } + return mesh; + } + + void TestEncodingCube() { + // Build a CornerTable looking at the mesh and verify that the caching of + // valences are reasonably correct and within range of expectations. This + // test is built specifically for working with 'cubes' and has expectations + // about the degree of each corner. + const std::string file_name = "cube_att.obj"; + std::unique_ptr in_mesh = DecodeObj(file_name); + ASSERT_NE(in_mesh, nullptr) << "Failed to load test model " << file_name; + draco::Mesh *mesh = nullptr; + mesh = in_mesh.get(); + + std::unique_ptr utable = + draco::CreateCornerTableFromPositionAttribute(mesh); + draco::CornerTable *table = utable.get(); + + table->GetValenceCache().CacheValences(); + table->GetValenceCache().CacheValencesInaccurate(); + + for (VertexIndex index = static_cast(0); + index < static_cast(table->num_vertices()); index++) { + const auto valence = table->Valence(index); + const auto valence2 = table->GetValenceCache().ValenceFromCache(index); + const auto valence3 = + table->GetValenceCache().ValenceFromCacheInaccurate(index); + ASSERT_EQ(valence, valence2); + ASSERT_GE(valence, valence3); // may be clipped. + + // No more than 6 triangles can touch a cube corner. + ASSERT_LE(valence, 6); + ASSERT_LE(valence2, 6); + ASSERT_LE(valence3, 6); + + // No less than 3 triangles can touch a cube corner. + ASSERT_GE(valence, 3); + ASSERT_GE(valence2, 3); + ASSERT_GE(valence3, 3); + } + + for (CornerIndex index = static_cast(0); + index < static_cast(table->num_corners()); index++) { + const auto valence = table->Valence(index); + const auto valence2 = table->GetValenceCache().ValenceFromCache(index); + const auto valence3 = + table->GetValenceCache().ValenceFromCacheInaccurate(index); + ASSERT_EQ(valence, valence2); + ASSERT_GE(valence, valence3); // may be clipped. + + // No more than 6 triangles can touch a cube corner, 6 edges result. + ASSERT_LE(valence, 6); + ASSERT_LE(valence2, 6); + ASSERT_LE(valence3, 6); + + // No less than 3 triangles can touch a cube corner, 3 edges result. + ASSERT_GE(valence, 3); + ASSERT_GE(valence2, 3); + ASSERT_GE(valence3, 3); + } + + table->GetValenceCache().ClearValenceCache(); + table->GetValenceCache().ClearValenceCacheInaccurate(); + } +}; + +TEST_F(CornerTableTest, NormalWithSeams) { TestEncodingCube(); } + +TEST_F(CornerTableTest, TestNonManifoldEdges) { + std::unique_ptr mesh = DecodeObj("non_manifold_wrap.obj"); + ASSERT_NE(mesh, nullptr); + std::unique_ptr ct = + draco::CreateCornerTableFromPositionAttribute(mesh.get()); + ASSERT_NE(ct, nullptr); + + MeshConnectedComponents connected_components; + connected_components.FindConnectedComponents(ct.get()); + ASSERT_EQ(connected_components.NumConnectedComponents(), 2); +} + +TEST_F(CornerTableTest, TestNewFace) { + // Tests that we can add a new face to the corner table. + const std::string file_name = "cube_att.obj"; + std::unique_ptr mesh = DecodeObj(file_name); + ASSERT_NE(mesh, nullptr); + + std::unique_ptr ct = + draco::CreateCornerTableFromPositionAttribute(mesh.get()); + ASSERT_NE(ct, nullptr); + ASSERT_EQ(ct->num_faces(), 12); + ASSERT_EQ(ct->num_corners(), 3 * 12); + ASSERT_EQ(ct->num_vertices(), 8); + + const VertexIndex new_vi = ct->AddNewVertex(); + ASSERT_EQ(ct->num_vertices(), 9); + + ASSERT_EQ(ct->AddNewFace({VertexIndex(6), VertexIndex(7), new_vi}), 12); + ASSERT_EQ(ct->num_faces(), 13); + ASSERT_EQ(ct->num_corners(), 3 * 13); + + ASSERT_EQ(ct->Vertex(CornerIndex(3 * 12 + 0)), 6); + ASSERT_EQ(ct->Vertex(CornerIndex(3 * 12) + 1), 7); + ASSERT_EQ(ct->Vertex(CornerIndex(3 * 12) + 2), new_vi); +} + +} // namespace draco diff --git a/contrib/draco/src/draco/mesh/mesh.cc b/contrib/draco/src/draco/mesh/mesh.cc index 3be4b1494..b287ecb45 100644 --- a/contrib/draco/src/draco/mesh/mesh.cc +++ b/contrib/draco/src/draco/mesh/mesh.cc @@ -15,6 +15,10 @@ #include "draco/mesh/mesh.h" #include +#include +#include +#include +#include namespace draco { @@ -22,7 +26,436 @@ namespace draco { template using conditional_t = typename std::conditional::type; +#ifdef DRACO_TRANSCODER_SUPPORTED +Mesh::Mesh() : compression_enabled_(false) {} +#else Mesh::Mesh() {} +#endif + +#ifdef DRACO_TRANSCODER_SUPPORTED +void Mesh::Copy(const Mesh &src) { + PointCloud::Copy(src); + name_ = src.name_; + faces_ = src.faces_; + attribute_data_ = src.attribute_data_; + material_library_.Copy(src.material_library_); + compression_enabled_ = src.compression_enabled_; + compression_options_ = src.compression_options_; + + // Copy mesh feature ID sets. + mesh_features_.clear(); + for (MeshFeaturesIndex i(0); i < src.NumMeshFeatures(); i++) { + std::unique_ptr mesh_features(new MeshFeatures()); + mesh_features->Copy(src.GetMeshFeatures(i)); + AddMeshFeatures(std::move(mesh_features)); + } + mesh_features_material_mask_ = src.mesh_features_material_mask_; + + // Copy non-material textures. + non_material_texture_library_.Copy(src.non_material_texture_library_); + + // Update pointers to non-material textures in mesh feature ID sets. + if (non_material_texture_library_.NumTextures() != 0) { + const auto texture_to_index_map = + src.non_material_texture_library_.ComputeTextureToIndexMap(); + for (MeshFeaturesIndex j(0); j < NumMeshFeatures(); ++j) { + Mesh::UpdateMeshFeaturesTexturePointer(texture_to_index_map, + &non_material_texture_library_, + &GetMeshFeatures(j)); + } + } + + // Copy structural metadata. + structural_metadata_.Copy(src.structural_metadata_); +} + +namespace { +// A helper struct that augments a point index with an attribute value index. +// A unique combination of |point_index| and |attribute_value_index| +// corresponds to a unique point on the mesh. Used to identify unique points +// after a new attribute is added to the mesh. +struct AugmentedPointData { + PointIndex point_index; + AttributeValueIndex attribute_value_index; + bool operator<(const AugmentedPointData &pd) const { + if (point_index < pd.point_index) { + return true; + } + if (point_index > pd.point_index) { + return false; + } + return attribute_value_index < pd.attribute_value_index; + } +}; +} // namespace + +int32_t Mesh::AddAttributeWithConnectivity( + std::unique_ptr att, + const IndexTypeVector &corner_to_value) { + // Map between augmented point and new point indices (one augmented point + // corresponds to one PointIndex). + std::map old_to_new_point_map; + + // Map between corners and the new point indices. + IndexTypeVector corner_to_point(num_faces() * 3, + kInvalidPointIndex); + + // Flag whether a given existing point index has been used. Used to ensure + // that mapping between existing and new point indices that are smaller + // than num_points() is identity. In other words, we want to keep indices of + // the existing points intact and add new points to end. + IndexTypeVector is_point_used(num_points(), false); + + int new_num_points = num_points(); + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + AugmentedPointData apd; + apd.point_index = CornerToPointId(ci); + apd.attribute_value_index = corner_to_value[ci]; + const auto it = old_to_new_point_map.find(apd); + if (it != old_to_new_point_map.end()) { + // Augmented point is already mapped to a point index. Reuse it. + corner_to_point[ci] = it->second; + } else { + // New combination of point index + attribute value index. Map it to a + // unique point index. + PointIndex new_point_index; + if (!is_point_used[apd.point_index]) { + // Reuse the existing (old) point index. + new_point_index = apd.point_index; + is_point_used[apd.point_index] = true; + } else { + // Add a new point index to the end. + new_point_index = PointIndex(new_num_points++); + } + old_to_new_point_map[apd] = new_point_index; + corner_to_point[ci] = new_point_index; + } + } + + // Update point to attribute value mapping for the new attribute. + att->SetExplicitMapping(new_num_points); + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + att->SetPointMapEntry(corner_to_point[ci], corner_to_value[ci]); + } + + // Update point to attribute value mapping on the remaining attributes if + // needed. + if (new_num_points > num_points()) { + set_num_points(new_num_points); + + // Setup attributes for the new number of points. + for (int ai = 0; ai < num_attributes(); ++ai) { + const bool mapping_was_identity = attribute(ai)->is_mapping_identity(); + attribute(ai)->SetExplicitMapping(new_num_points); + if (mapping_was_identity) { + // Convert all old points from identity to explicit mapping. + for (AttributeValueIndex avi(0); avi < attribute(ai)->size(); ++avi) { + attribute(ai)->SetPointMapEntry(PointIndex(avi.value()), avi); + } + } + } + + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + const PointIndex old_point_index = CornerToPointId(ci); + const PointIndex new_point_index = corner_to_point[ci]; + if (old_point_index == new_point_index) { + continue; + } + // Update point to value mapping for all existing attributes. + for (int ai = 0; ai < num_attributes(); ++ai) { + attribute(ai)->SetPointMapEntry( + new_point_index, attribute(ai)->mapped_index(old_point_index)); + } + // Update mapping between the corner and the new point index. + faces_[FaceIndex(ci.value() / 3)][ci.value() % 3] = new_point_index; + } + } + + // If any of the old points have not been used, initialize dummy mapping for + // the new attribute. + for (PointIndex pi(0); pi < is_point_used.size(); ++pi) { + if (!is_point_used[pi]) { + att->SetPointMapEntry(pi, AttributeValueIndex(0)); + } + } + + return PointCloud::AddAttribute(std::move(att)); +} + +int32_t Mesh::AddPerVertexAttribute(std::unique_ptr att) { + const PointAttribute *const pos_att = + GetNamedAttribute(GeometryAttribute::POSITION); + if (pos_att == nullptr) { + return -1; + } + if (att->size() != pos_att->size()) { + return -1; // Number of values must be same as in the position attribute. + } + + if (pos_att->is_mapping_identity()) { + att->SetIdentityMapping(); + } else { + // Copy point to attribute value mapping from the position attribute to + // |att|. + att->SetExplicitMapping(num_points()); + for (PointIndex pi(0); pi < num_points(); ++pi) { + att->SetPointMapEntry(pi, pos_att->mapped_index(pi)); + } + } + + return PointCloud::AddAttribute(std::move(att)); +} + +void Mesh::RemoveIsolatedPoints() { + // For each point, check if it is mapped to a face. + IndexTypeVector is_point_used(num_points(), false); + int num_used_points = 0; + for (FaceIndex fi(0); fi < num_faces(); ++fi) { + const auto &f = face(fi); + for (int c = 0; c < 3; ++c) { + if (!is_point_used[f[c]]) { + num_used_points++; + is_point_used[f[c]] = true; + } + } + } + if (num_used_points == num_points()) { + return; // All points are used. + } + + // Create mapping between the old and new point indices. + IndexTypeVector old_to_new_point_map( + num_points(), kInvalidPointIndex); + PointIndex new_point_index(0); + for (PointIndex pi(0); pi < num_points(); ++pi) { + if (is_point_used[pi]) { + old_to_new_point_map[pi] = new_point_index++; + } + } + + // Update point to attribute value index map for all attributes. + for (int ai = 0; ai < num_attributes(); ++ai) { + PointAttribute *att = attribute(ai); + if (att->is_mapping_identity()) { + // When the attribute uses identity mapping we need to reorder to the + // attribute values to match the new point indices. + for (PointIndex pi(0); pi < num_points(); ++pi) { + const PointIndex new_pi = old_to_new_point_map[pi]; + if (new_pi == pi || new_pi == kInvalidPointIndex) { + continue; + } + att->SetAttributeValue( + AttributeValueIndex(new_pi.value()), + att->GetAddress(AttributeValueIndex(pi.value()))); + } + att->Resize(num_used_points); + } else { + // For explicitly mapped attributes, we first update the point to + // attribute value mapping and then we remove all unused values from the + // attribute. + for (PointIndex pi(0); pi < num_points(); ++pi) { + const PointIndex new_pi = old_to_new_point_map[pi]; + if (new_pi == pi || new_pi == kInvalidPointIndex) { + continue; + } + att->SetPointMapEntry(new_pi, att->mapped_index(pi)); + } + att->SetExplicitMapping(num_used_points); + + att->RemoveUnusedValues(); + } + } + + // Update the mapping between faces and point indices. + for (FaceIndex fi(0); fi < num_faces(); ++fi) { + auto &f = faces_[fi]; + for (int c = 0; c < 3; ++c) { + f[c] = old_to_new_point_map[f[c]]; + } + } + + set_num_points(num_used_points); +} + +void Mesh::RemoveUnusedMaterials() { RemoveUnusedMaterials(true); } + +void Mesh::RemoveUnusedMaterials(bool remove_unused_material_indices) { + const int mat_att_index = GetNamedAttributeId(GeometryAttribute::MATERIAL); + if (mat_att_index == -1) { + // Remove all materials except for the first one. + while (GetMaterialLibrary().NumMaterials() > 1) { + GetMaterialLibrary().RemoveMaterial(1); + } + GetMaterialLibrary().RemoveUnusedTextures(); + return; + } + auto mat_att = attribute(mat_att_index); + + // Deduplicate attribute values in the material attribute to ensure that one + // attribute value index corresponds to one unique material index. + // Note that this does not remove unused material indices. + mat_att->DeduplicateValues(*mat_att); + + // Gather all material indices that are referenced by faces of the mesh. + const int num_materials = GetMaterialLibrary().NumMaterials(); + std::vector is_material_used(num_materials, false); + int num_used_materials = 0; + + // Helper function that updates |is_material_used| for the processed mesh. + auto update_used_materials = [&is_material_used, &num_used_materials, mat_att, + num_materials](PointIndex pi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(pi, &mat_index); + if (mat_index < num_materials) { + if (!is_material_used[mat_index]) { + is_material_used[mat_index] = true; + num_used_materials++; + } + } + }; + + if (num_faces() > 0) { + for (FaceIndex fi(0); fi < num_faces(); ++fi) { + update_used_materials(faces_[fi][0]); + } + } else { + // Handle the mesh as a point cloud and check materials used by points. + for (PointIndex pi(0); pi < num_points(); ++pi) { + update_used_materials(pi); + } + } + + // Check if any of the (unused) materials is used by mesh features. If so, + // user should remove unused mesh features first. + for (MeshFeaturesIndex mfi(0); mfi < NumMeshFeatures(); ++mfi) { + for (int mask_index = 0; mask_index < NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + const int mat_index = GetMeshFeaturesMaterialMask(mfi, mask_index); + if (mat_index < num_materials && !is_material_used[mat_index]) { + is_material_used[mat_index] = true; + num_used_materials++; + } + } + } + + if (num_used_materials == num_materials) { + return; // All materials are used, don't do anything. + } + + // Remove unused materials from the material library or replace them with + // default materials if we do not remove unused material indices. + for (int mi = num_materials - 1; mi >= 0; --mi) { + if (!is_material_used[mi] && mi < GetMaterialLibrary().NumMaterials()) { + if (remove_unused_material_indices) { + GetMaterialLibrary().RemoveMaterial(mi); + } else { + GetMaterialLibrary().MutableMaterial(mi)->Clear(); + } + } + } + GetMaterialLibrary().RemoveUnusedTextures(); + + if (!remove_unused_material_indices) { + // All the code below handles updating of material indices. Since we do not + // want to update them, we can return early. + return; + } + + // Compute map between old and new material indices. + std::vector old_to_new_material_index_map(num_materials, -1); + for (int mi = 0, new_material_index = 0; mi < num_materials; ++mi) { + if (is_material_used[mi]) { + old_to_new_material_index_map[mi] = new_material_index; + ++new_material_index; + } + } + IndexTypeVector + old_to_new_material_attribute_value_index_map(mat_att->size(), -1); + for (AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + if (mat_index < num_materials && is_material_used[mat_index]) { + old_to_new_material_attribute_value_index_map[avi] = + old_to_new_material_index_map[mat_index]; + } + } + + // Update attribute values with the new number of materials. + mat_att->Reset(num_used_materials); + + // Set identity mapping between AttributeValueIndex and material indices. + for (AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + const uint32_t mat_index = avi.value(); + mat_att->SetAttributeValue(avi, &mat_index); + } + + // Update mapping between points and attribute values. + for (PointIndex pi(0); pi < num_points(); ++pi) { + const AttributeValueIndex old_avi = mat_att->mapped_index(pi); + mat_att->SetPointMapEntry( + pi, AttributeValueIndex( + old_to_new_material_attribute_value_index_map[old_avi])); + } + + // Update material indices on mesh features. + for (MeshFeaturesIndex mfi(0); mfi < NumMeshFeatures(); ++mfi) { + for (int mask_index = 0; mask_index < NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + const int old_mat_index = GetMeshFeaturesMaterialMask(mfi, mask_index); + if (old_mat_index < num_materials && is_material_used[old_mat_index]) { + mesh_features_material_mask_[mfi][mask_index] = + old_to_new_material_index_map[old_mat_index]; + } + } + } +} + +void Mesh::UpdateMeshFeaturesTexturePointer( + const std::unordered_map &texture_to_index_map, + TextureLibrary *texture_library, MeshFeatures *mesh_features) { + TextureMap &texture_map = mesh_features->GetTextureMap(); + if (texture_map.texture() == nullptr) { + return; + } + const auto it = texture_to_index_map.find(texture_map.texture()); + DRACO_DCHECK(it != texture_to_index_map.end()); + const int texture_index = it->second; + DRACO_DCHECK(texture_index < texture_library->NumTextures()); + texture_map.SetTexture(texture_library->GetTexture(texture_index)); +} + +void Mesh::CopyMeshFeaturesForMaterial(const Mesh &source_mesh, + Mesh *target_mesh, int material_index) { + for (MeshFeaturesIndex mfi(0); mfi < source_mesh.NumMeshFeatures(); ++mfi) { + // Mesh features is used if it doesn't have any material mask or if one + // of the material masks matches |material_index|. + bool is_used = source_mesh.NumMeshFeaturesMaterialMasks(mfi) == 0; + for (int mask_index = 0; + !is_used && mask_index < source_mesh.NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + if (source_mesh.GetMeshFeaturesMaterialMask(mfi, mask_index) == + material_index) { + is_used = true; + } + } + if (is_used) { + // Copy over the mesh features to the target mesh. Note that texture + // pointers are not updated at this step. + std::unique_ptr new_mf(new MeshFeatures()); + new_mf->Copy(source_mesh.GetMeshFeatures(mfi)); + target_mesh->AddMeshFeatures(std::move(new_mf)); + } + } +} + +int32_t Mesh::AddPerFaceAttribute(std::unique_ptr att) { + IndexTypeVector corner_map(num_faces() * 3); + for (CornerIndex ci(0); ci < num_faces() * 3; ++ci) { + corner_map[ci] = AttributeValueIndex(ci.value() / 3); + } + return AddAttributeWithConnectivity(std::move(att), corner_map); +} +#endif // DRACO_TRANSCODER_SUPPORTED #ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED void Mesh::ApplyPointIdDeduplication( diff --git a/contrib/draco/src/draco/mesh/mesh.h b/contrib/draco/src/draco/mesh/mesh.h index f4506da81..652c2c010 100644 --- a/contrib/draco/src/draco/mesh/mesh.h +++ b/contrib/draco/src/draco/mesh/mesh.h @@ -16,12 +16,20 @@ #define DRACO_MESH_MESH_H_ #include +#include #include "draco/attributes/geometry_indices.h" #include "draco/core/hash_utils.h" #include "draco/core/macros.h" #include "draco/core/status.h" #include "draco/draco_features.h" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/compression/draco_compression_options.h" +#include "draco/material/material_library.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/mesh_indices.h" +#include "draco/metadata/structural_metadata.h" +#endif #include "draco/point_cloud/point_cloud.h" namespace draco { @@ -47,6 +55,11 @@ class Mesh : public PointCloud { Mesh(); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copies all data from the |src| mesh. + void Copy(const Mesh &src); +#endif + void AddFace(const Face &face) { faces_.push_back(face); } void SetFace(FaceIndex face_id, const Face &face) { @@ -83,6 +96,38 @@ class Mesh : public PointCloud { } } +#ifdef DRACO_TRANSCODER_SUPPORTED + // Adds a point attribute |att| to the mesh and returns the index of the + // newly inserted attribute. Attribute connectivity data is specified in + // |corner_to_value| array that contains mapping between face corners and + // attribute value indices. + // The purpose of this function is to allow users to add attributes with + // arbitrary connectivity to an existing mesh. New points will be + // automatically created if needed. + int32_t AddAttributeWithConnectivity( + std::unique_ptr att, + const IndexTypeVector &corner_to_value); + + // Adds a point attribute |att| to the mesh and returns the index of the + // newly inserted attribute. The inserted attribute must have the same + // connectivity as the position attribute of the mesh (that is, the attribute + // values are defined per-vertex). Each attribute value entry in |att| + // corresponds to the corresponding attribute value entry in the position + // attribute (AttributeValueIndex in both attributes refer to the same + // spatial vertex). + // Returns -1 in case of error. + int32_t AddPerVertexAttribute(std::unique_ptr att); + + // Removes points that are not mapped to any face of the mesh. All attribute + // values are going to be removed as well. + void RemoveIsolatedPoints(); + + // Adds a point attribute |att| to the mesh and returns the index of the + // newly inserted attribute. Attribute values are mapped 1:1 to face indices. + // Returns -1 in case of error. + int32_t AddPerFaceAttribute(std::unique_ptr att); +#endif // DRACO_TRANSCODER_SUPPORTED + MeshAttributeElementType GetAttributeElementType(int att_id) const { return attribute_data_[att_id].element_type; } @@ -109,6 +154,103 @@ class Mesh : public PointCloud { MeshAttributeElementType element_type; }; +#ifdef DRACO_TRANSCODER_SUPPORTED + void SetName(const std::string &name) { name_ = name; } + const std::string &GetName() const { return name_; } + const MaterialLibrary &GetMaterialLibrary() const { + return material_library_; + } + MaterialLibrary &GetMaterialLibrary() { return material_library_; } + + // Removes all materials that are not referenced by any face of the mesh. + // Optional argument |remove_unused_material_indices| can be used to control + // whether unusued material indices are removed as well (default = true). + // If material indices are not removed, the unused material indices will + // point to empty (default) materials. + void RemoveUnusedMaterials(); + void RemoveUnusedMaterials(bool remove_unused_material_indices); + + // Enables or disables Draco geometry compression for this mesh. + void SetCompressionEnabled(bool enabled) { compression_enabled_ = enabled; } + bool IsCompressionEnabled() const { return compression_enabled_; } + + // Sets |options| that configure Draco geometry compression. This does not + // enable or disable compression. + void SetCompressionOptions(const DracoCompressionOptions &options) { + compression_options_ = options; + } + const DracoCompressionOptions &GetCompressionOptions() const { + return compression_options_; + } + DracoCompressionOptions &GetCompressionOptions() { + return compression_options_; + } + + // Library that contains non-material textures. + const TextureLibrary &GetNonMaterialTextureLibrary() const { + return non_material_texture_library_; + } + TextureLibrary &GetNonMaterialTextureLibrary() { + return non_material_texture_library_; + } + + // Mesh feature ID sets as defined by EXT_mesh_features glTF extension. + MeshFeaturesIndex AddMeshFeatures( + std::unique_ptr mesh_features) { + mesh_features_.push_back(std::move(mesh_features)); + mesh_features_material_mask_.push_back({}); + return MeshFeaturesIndex(mesh_features_.size() - 1); + } + int NumMeshFeatures() const { return mesh_features_.size(); } + const MeshFeatures &GetMeshFeatures(MeshFeaturesIndex index) const { + return *mesh_features_[index]; + } + MeshFeatures &GetMeshFeatures(MeshFeaturesIndex index) { + return *mesh_features_[index]; + } + void RemoveMeshFeatures(MeshFeaturesIndex index) { + mesh_features_.erase(mesh_features_.begin() + index.value()); + mesh_features_material_mask_.erase(mesh_features_material_mask_.begin() + + index.value()); + } + + // Restricts given mesh features to faces mapped to a material with + // |material_index|. Note that single mesh features can be restricted to + // multiple materials. + void AddMeshFeaturesMaterialMask(MeshFeaturesIndex index, + int material_index) { + mesh_features_material_mask_[index].push_back(material_index); + } + + size_t NumMeshFeaturesMaterialMasks(MeshFeaturesIndex index) const { + return mesh_features_material_mask_[index].size(); + } + int GetMeshFeaturesMaterialMask(MeshFeaturesIndex index, + int mask_index) const { + return mesh_features_material_mask_[index][mask_index]; + } + + // Updates mesh features texture pointer to point to a new |texture_library|. + // The current texture pointer is used to determine the texture index in the + // new texture library via a given |texture_to_index_map|. + static void UpdateMeshFeaturesTexturePointer( + const std::unordered_map &texture_to_index_map, + TextureLibrary *texture_library, MeshFeatures *mesh_features); + + // Copies over mesh features from |source_mesh| and stores them in + // |target_mesh| as long as the mesh features material mask is valid for + // given |material_index|. + static void CopyMeshFeaturesForMaterial(const Mesh &source_mesh, + Mesh *target_mesh, + int material_index); + + // Structural metadata. + const StructuralMetadata &GetStructuralMetadata() const { + return structural_metadata_; + } + StructuralMetadata &GetStructuralMetadata() { return structural_metadata_; } +#endif // DRACO_TRANSCODER_SUPPORTED + protected: #ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED // Extends the point deduplication to face corners. This method is called from @@ -119,6 +261,10 @@ class Mesh : public PointCloud { const std::vector &unique_point_ids) override; #endif + // Exposes |faces_|. Use |faces_| at your own risk. DO NOT store the + // reference: the |faces_| object is destroyed with the mesh. + IndexTypeVector &faces() { return faces_; } + private: // Mesh specific per-attribute data. std::vector attribute_data_; @@ -127,6 +273,40 @@ class Mesh : public PointCloud { // that converts vertex indices into attribute indices. IndexTypeVector faces_; +#ifdef DRACO_TRANSCODER_SUPPORTED + // Mesh name. + std::string name_; + + // Materials applied to to this mesh. + MaterialLibrary material_library_; + + // Compression options for this mesh. + // TODO(vytyaz): Store encoded bitstream that this mesh compresses into. + bool compression_enabled_; + DracoCompressionOptions compression_options_; + + // Sets of feature IDs as defined by EXT_mesh_features glTF extension. + IndexTypeVector> + mesh_features_; + + // When the Mesh contains multiple materials, |mesh_features_material_mask_| + // can be used to limit specific MeshFeaturesIndex to a vector of material + // indices. If for a given mesh feature index, the material indices are empty, + // the corresponding mesh features are applied to the entire mesh. + IndexTypeVector> + mesh_features_material_mask_; + + // Texture library for storing non-material textures used by this mesh, e.g., + // textures containing mesh feature IDs of EXT_mesh_features glTF extension. + // If the mesh is part of the scene then the textures are stored in the scene. + // Note that mesh features contain pointers to non-material textures. It is + // responsibility of class user to update these pointers when updating the + // textures. See Mesh::Copy() for example. + TextureLibrary non_material_texture_library_; + + // Structural metadata defined by the EXT_structural_metadata glTF extension. + StructuralMetadata structural_metadata_; +#endif // DRACO_TRANSCODER_SUPPORTED friend struct MeshHasher; }; diff --git a/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc b/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc index b832379af..305811f10 100644 --- a/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc +++ b/contrib/draco/src/draco/mesh/mesh_are_equivalent.cc @@ -15,6 +15,9 @@ #include "draco/mesh/mesh_are_equivalent.h" #include +#include + +#include "draco/texture/texture_utils.h" namespace draco { @@ -114,6 +117,55 @@ bool MeshAreEquivalent::operator()(const Mesh &mesh0, const Mesh &mesh1) { // face with respect to lex order. Init(mesh0, mesh1); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Compare geometry compression settings. + if (mesh0.IsCompressionEnabled() != mesh1.IsCompressionEnabled()) { + return false; + } + if (mesh0.GetCompressionOptions() != mesh1.GetCompressionOptions()) { + return false; + } + + // Compare non-material texture library sizes. + if (mesh0.GetNonMaterialTextureLibrary().NumTextures() != + mesh1.GetNonMaterialTextureLibrary().NumTextures()) { + return false; + } + + // Compare mesh feature ID sets. + if (mesh0.NumMeshFeatures() != mesh1.NumMeshFeatures()) { + return false; + } + for (MeshFeaturesIndex i(0); i < mesh0.NumMeshFeatures(); ++i) { + const MeshFeatures &features0 = mesh0.GetMeshFeatures(i); + const MeshFeatures &features1 = mesh1.GetMeshFeatures(i); + if (features0.GetAttributeIndex() != features1.GetAttributeIndex()) { + return false; + } + if (features0.GetFeatureCount() != features1.GetFeatureCount()) { + return false; + } + if (features0.GetLabel() != features1.GetLabel()) { + return false; + } + if (features0.GetNullFeatureId() != features1.GetNullFeatureId()) { + return false; + } + if (features0.GetTextureChannels() != features1.GetTextureChannels()) { + return false; + } + if (features0.GetPropertyTableIndex() != + features1.GetPropertyTableIndex()) { + return false; + } + const TextureMap &map0 = features0.GetTextureMap(); + const TextureMap &map1 = features1.GetTextureMap(); + if (map0.tex_coord_index() != map1.tex_coord_index()) { + return false; + } + } +#endif // DRACO_TRANSCODER_SUPPORTED + // Check for every attribute that is valid that every corner is identical. typedef GeometryAttribute::Type AttributeType; const int att_max = AttributeType::NAMED_ATTRIBUTES_COUNT; diff --git a/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc b/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc index 74db3f7de..94d8c9c16 100644 --- a/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc +++ b/contrib/draco/src/draco/mesh/mesh_are_equivalent_test.cc @@ -15,6 +15,7 @@ #include "draco/mesh/mesh_are_equivalent.h" #include +#include #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" @@ -30,6 +31,14 @@ TEST_F(MeshAreEquivalentTest, TestOnIndenticalMesh) { const std::string file_name = "test_nm.obj"; const std::unique_ptr mesh(ReadMeshFromTestFile(file_name)); ASSERT_NE(mesh, nullptr) << "Failed to load test model." << file_name; + +#ifdef DRACO_TRANSCODER_SUPPORTED + // Add mesh feature ID set to the mesh. + std::unique_ptr mesh_features(new MeshFeatures()); + mesh->AddMeshFeatures(std::move(mesh_features)); +#endif + + // Check that mesh is equivalent to itself. MeshAreEquivalent equiv; ASSERT_TRUE(equiv(*mesh, *mesh)); } @@ -95,4 +104,32 @@ TEST_F(MeshAreEquivalentTest, TestOnBigMesh) { ASSERT_TRUE(equiv(*mesh0, *mesh1)); } +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST_F(MeshAreEquivalentTest, TestMeshFeatures) { + const std::string file_name = "test_nm.obj"; + const std::unique_ptr mesh0(ReadMeshFromTestFile(file_name)); + const std::unique_ptr mesh1(ReadMeshFromTestFile(file_name)); + ASSERT_NE(mesh0, nullptr); + ASSERT_NE(mesh1, nullptr); + + // Add identical mesh feature ID sets to meshes. + mesh0->AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + mesh1->AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + + // Empty feature sets should match. + MeshAreEquivalent equiv; + ASSERT_TRUE(equiv(*mesh0, *mesh1)); + + // Make mesh features different and check that the meshes are not equivalent. + mesh0->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(5); + mesh1->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(6); + ASSERT_FALSE(equiv(*mesh0, *mesh1)); + + // Make mesh features identical and check that the meshes are equivalent. + mesh0->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + mesh1->GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + ASSERT_TRUE(equiv(*mesh0, *mesh1)); +} +#endif // DRACO_TRANSCODER_SUPPORTED } // namespace draco diff --git a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc index 28b68d5fd..54801ce5c 100644 --- a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc +++ b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.cc @@ -126,18 +126,18 @@ void MeshAttributeCornerTable::AddSeamEdge(CornerIndex c) { } } -void MeshAttributeCornerTable::RecomputeVertices(const Mesh *mesh, +bool MeshAttributeCornerTable::RecomputeVertices(const Mesh *mesh, const PointAttribute *att) { DRACO_DCHECK(GetValenceCache().IsCacheEmpty()); if (mesh != nullptr && att != nullptr) { - RecomputeVerticesInternal(mesh, att); + return RecomputeVerticesInternal(mesh, att); } else { - RecomputeVerticesInternal(nullptr, nullptr); + return RecomputeVerticesInternal(nullptr, nullptr); } } template -void MeshAttributeCornerTable::RecomputeVerticesInternal( +bool MeshAttributeCornerTable::RecomputeVerticesInternal( const Mesh *mesh, const PointAttribute *att) { DRACO_DCHECK(GetValenceCache().IsCacheEmpty()); vertex_to_attribute_entry_id_map_.clear(); @@ -167,6 +167,11 @@ void MeshAttributeCornerTable::RecomputeVerticesInternal( while (act_c != kInvalidCornerIndex) { first_c = act_c; act_c = SwingLeft(act_c); + if (act_c == c) { + // We reached the initial corner which shouldn't happen when we swing + // left from |c|. + return false; + } } } corner_to_vertex_map_[first_c.value()] = VertexIndex(first_vert_id.value()); @@ -189,6 +194,7 @@ void MeshAttributeCornerTable::RecomputeVerticesInternal( act_c = corner_table_->SwingRight(act_c); } } + return true; } int MeshAttributeCornerTable::Valence(VertexIndex v) const { diff --git a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h index 7dad25cf1..c60be7c86 100644 --- a/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h +++ b/contrib/draco/src/draco/mesh/mesh_attribute_corner_table.h @@ -40,7 +40,7 @@ class MeshAttributeCornerTable { // whenever the seam edges are updated). // |mesh| and |att| can be null, in which case mapping between vertices and // attribute value ids is set to identity. - void RecomputeVertices(const Mesh *mesh, const PointAttribute *att); + bool RecomputeVertices(const Mesh *mesh, const PointAttribute *att); inline bool IsCornerOppositeToSeamEdge(CornerIndex corner) const { return is_edge_on_seam_[corner.value()]; @@ -130,6 +130,12 @@ class MeshAttributeCornerTable { return false; } + bool IsDegenerated(FaceIndex face) const { + // Introducing seams can't change the degeneracy of the individual faces, + // therefore we can delegate the check to the original |corner_table_|. + return corner_table_->IsDegenerated(face); + } + bool no_interior_seams() const { return no_interior_seams_; } const CornerTable *corner_table() const { return corner_table_; } @@ -166,7 +172,7 @@ class MeshAttributeCornerTable { private: template - void RecomputeVerticesInternal(const Mesh *mesh, const PointAttribute *att); + bool RecomputeVerticesInternal(const Mesh *mesh, const PointAttribute *att); std::vector is_edge_on_seam_; std::vector is_vertex_on_seam_; diff --git a/contrib/draco/src/draco/mesh/mesh_cleanup.cc b/contrib/draco/src/draco/mesh/mesh_cleanup.cc index 75b55f045..a6dc1823e 100644 --- a/contrib/draco/src/draco/mesh/mesh_cleanup.cc +++ b/contrib/draco/src/draco/mesh/mesh_cleanup.cc @@ -14,21 +14,25 @@ // #include "draco/mesh/mesh_cleanup.h" +#include +#include #include +#include +#include #include "draco/core/hash_utils.h" namespace draco { -bool MeshCleanup::operator()(Mesh *mesh, const MeshCleanupOptions &options) { +Status MeshCleanup::Cleanup(Mesh *mesh, const MeshCleanupOptions &options) { if (!options.remove_degenerated_faces && !options.remove_unused_attributes && !options.remove_duplicate_faces && !options.make_geometry_manifold) { - return true; // Nothing to cleanup. + return OkStatus(); // Nothing to cleanup. } const PointAttribute *const pos_att = mesh->GetNamedAttribute(GeometryAttribute::POSITION); if (pos_att == nullptr) { - return false; + return Status(Status::DRACO_ERROR, "Missing position attribute."); } if (options.remove_degenerated_faces) { @@ -43,7 +47,7 @@ bool MeshCleanup::operator()(Mesh *mesh, const MeshCleanupOptions &options) { RemoveUnusedAttributes(mesh); } - return true; + return OkStatus(); } void MeshCleanup::RemoveDegeneratedFaces(Mesh *mesh) { diff --git a/contrib/draco/src/draco/mesh/mesh_cleanup.h b/contrib/draco/src/draco/mesh/mesh_cleanup.h index 09aae2e1c..c6bdfc6c0 100644 --- a/contrib/draco/src/draco/mesh/mesh_cleanup.h +++ b/contrib/draco/src/draco/mesh/mesh_cleanup.h @@ -16,42 +16,38 @@ #define DRACO_MESH_MESH_CLEANUP_H_ #include "draco/core/status.h" +#include "draco/draco_features.h" #include "draco/mesh/mesh.h" namespace draco { // Options used by the MeshCleanup class. struct MeshCleanupOptions { - MeshCleanupOptions() - : remove_degenerated_faces(true), - remove_duplicate_faces(true), - remove_unused_attributes(true), - make_geometry_manifold(false) {} // If true, the cleanup tool removes any face where two or more vertices // share the same position index. - bool remove_degenerated_faces; + bool remove_degenerated_faces = true; // If true, the cleanup tool removes all duplicate faces. A pair of faces is // duplicate if both faces share the same position indices on all vertices // (that is, position values have to be duduplicated). Note that all // non-position properties are currently ignored. - bool remove_duplicate_faces; + bool remove_duplicate_faces = true; // If true, the cleanup tool removes any unused attribute value or unused // point id. For example, it can be used to remove isolated vertices. - bool remove_unused_attributes; + bool remove_unused_attributes = true; // If true, the cleanup tool splits vertices along non-manifold edges and // vertices. This ensures that the connectivity defined by position indices // is manifold. - bool make_geometry_manifold; + bool make_geometry_manifold = false; }; // Tool that can be used for removing bad or unused data from draco::Meshes. class MeshCleanup { public: // Performs in-place cleanup of the input mesh according to the input options. - bool operator()(Mesh *mesh, const MeshCleanupOptions &options); + static Status Cleanup(Mesh *mesh, const MeshCleanupOptions &options); private: static void RemoveDegeneratedFaces(Mesh *mesh); diff --git a/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc b/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc index 89c350e94..76e5206ae 100644 --- a/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc +++ b/contrib/draco/src/draco/mesh/mesh_cleanup_test.cc @@ -15,6 +15,7 @@ #include "draco/mesh/mesh_cleanup.h" #include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" #include "draco/core/vector_d.h" #include "draco/mesh/triangle_soup_mesh_builder.h" @@ -43,9 +44,7 @@ TEST_F(MeshCleanupTest, TestDegneratedFaces) { ASSERT_NE(mesh, nullptr) << "Failed to build the test mesh."; ASSERT_EQ(mesh->num_faces(), 2) << "Wrong number of faces in the input mesh."; MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)) - << "Failed to cleanup the mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 1) << "Failed to remove degenerated faces."; } @@ -89,9 +88,7 @@ TEST_F(MeshCleanupTest, TestDegneratedFacesAndIsolatedVertices) { << "Wrong number of point ids in the input mesh."; ASSERT_EQ(mesh->attribute(int_att_id)->size(), 3); const MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)) - << "Failed to cleanup the mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 1) << "Failed to remove degenerated faces."; ASSERT_EQ(mesh->num_points(), 3) << "Failed to remove isolated attribute indices."; @@ -133,9 +130,7 @@ TEST_F(MeshCleanupTest, TestAttributes) { ASSERT_EQ(mesh->attribute(1)->size(), 2u) << "Wrong number of generic attribute entries."; const MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)) - << "Failed to cleanup the mesh."; + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 1) << "Failed to remove degenerated faces."; ASSERT_EQ(mesh->num_points(), 3) << "Failed to remove isolated attribute indices."; @@ -184,8 +179,7 @@ TEST_F(MeshCleanupTest, TestDuplicateFaces) { ASSERT_NE(mesh, nullptr); ASSERT_EQ(mesh->num_faces(), 5); const MeshCleanupOptions cleanup_options; - MeshCleanup cleanup; - ASSERT_TRUE(cleanup(mesh.get(), cleanup_options)); + DRACO_ASSERT_OK(MeshCleanup::Cleanup(mesh.get(), cleanup_options)); ASSERT_EQ(mesh->num_faces(), 2); } diff --git a/contrib/draco/src/draco/mesh/mesh_connected_components.h b/contrib/draco/src/draco/mesh/mesh_connected_components.h new file mode 100644 index 000000000..6ee30551e --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_connected_components.h @@ -0,0 +1,161 @@ +// Copyright 2016 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_MESH_MESH_CONNECTED_COMPONENTS_H_ +#define DRACO_MESH_MESH_CONNECTED_COMPONENTS_H_ + +#include + +#include "draco/mesh/corner_table.h" + +namespace draco { + +// Class for detecting connected components on an input mesh defined by a +// corner table. Degenerated faces and their vertices are not assigned to any +// component. +class MeshConnectedComponents { + public: + MeshConnectedComponents() = default; + + // Initializes the class with the component data of the input mesh. No other + // method should be called before this one. + template + void FindConnectedComponents(const CornerTableT *corner_table); + int NumConnectedComponents() const { return components_.size(); } + + struct ConnectedComponent { + std::vector vertices; + std::vector faces; + std::vector boundary_edges; + }; + + const ConnectedComponent &GetConnectedComponent(int index) const { + return components_[index]; + } + + // Returns the id of an component attached to a given vertex. Returns -1 when + // the vertex was not assigned to any component. + int GetConnectedComponentIdAtVertex(int vertex_id) const { + return vertex_to_component_map_[vertex_id]; + } + + // Returns the number of vertices that belong to the input component. + int NumConnectedComponentVertices(int component_id) const { + return components_[component_id].vertices.size(); + } + + // Returns the i-th vertex of the input component. + int GetConnectedComponentVertex(int component_id, int i) const { + return components_[component_id].vertices[i]; + } + + // Returns the id of an component attached to a given face. Returns -1 when + // the face was not assigned to any component. + int GetConnectedComponentIdAtFace(int face_id) const { + return face_to_component_map_[face_id]; + } + + // Returns the number of faces that belong to the input component. + int NumConnectedComponentFaces(int component_id) const { + return components_[component_id].faces.size(); + } + + // Returns the i-th face of the input component. + int GetConnectedComponentFace(int component_id, int i) const { + return components_[component_id].faces[i]; + } + + // Returns the number of boundary edges that belong to the input component. + int NumConnectedComponentBoundaryEdges(int component_id) const { + return components_[component_id].boundary_edges.size(); + } + + // Returns the i-th boundary edge of the input component. + int GetConnectedComponentBoundaryEdge(int component_id, int i) const { + return components_[component_id].boundary_edges[i]; + } + + private: + std::vector vertex_to_component_map_; + std::vector face_to_component_map_; + std::vector boundary_corner_to_component_map_; + std::vector components_; +}; + +template +void MeshConnectedComponents::FindConnectedComponents( + const CornerTableT *corner_table) { + components_.clear(); + vertex_to_component_map_.assign(corner_table->num_vertices(), -1); + face_to_component_map_.assign(corner_table->num_faces(), -1); + boundary_corner_to_component_map_.assign(corner_table->num_corners(), -1); + std::vector is_face_visited(corner_table->num_faces(), false); + std::vector face_stack; + // Go over all faces of the mesh and for each unvisited face, recursively + // traverse its neighborhood and mark all traversed faces as visited. All + // faces visited during one traversal belong to one mesh component. + for (int face_id = 0; face_id < corner_table->num_faces(); ++face_id) { + if (is_face_visited[face_id]) { + continue; + } + if (corner_table->IsDegenerated(FaceIndex(face_id))) { + continue; + } + const int component_id = components_.size(); + components_.push_back(ConnectedComponent()); + face_stack.push_back(face_id); + is_face_visited[face_id] = true; + while (!face_stack.empty()) { + const int act_face_id = face_stack.back(); + if (face_to_component_map_[act_face_id] == -1) { + face_to_component_map_[act_face_id] = component_id; + components_[component_id].faces.push_back(act_face_id); + } + face_stack.pop_back(); + // Gather all neighboring faces. + std::array corners = + corner_table->AllCorners(FaceIndex(act_face_id)); + for (int c = 0; c < 3; ++c) { + // Update vertex to component mapping. + const int vertex_id = corner_table->Vertex(corners[c]).value(); + if (vertex_to_component_map_[vertex_id] == -1) { + vertex_to_component_map_[vertex_id] = component_id; + components_[component_id].vertices.push_back(vertex_id); + } + // Traverse component to neighboring faces (add the faces to the stack). + const CornerIndex opp_corner = corner_table->Opposite(corners[c]); + if (opp_corner == kInvalidCornerIndex) { + if (boundary_corner_to_component_map_[corners[c].value()] == -1) { + boundary_corner_to_component_map_[corners[c].value()] = + component_id; + components_[component_id].boundary_edges.push_back( + corners[c].value()); + } + continue; // Invalid corner (mesh boundary). + } + + const int opp_face_id = corner_table->Face(opp_corner).value(); + if (is_face_visited[opp_face_id]) { + continue; // Opposite face has been already reached. + } + is_face_visited[opp_face_id] = true; + face_stack.push_back(opp_face_id); + } + } + } +} + +} // namespace draco + +#endif // DRACO_MESH_MESH_CONNECTED_COMPONENTS_H_ diff --git a/contrib/draco/src/draco/mesh/mesh_features.cc b/contrib/draco/src/draco/mesh/mesh_features.cc new file mode 100644 index 000000000..f859ae411 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_features.cc @@ -0,0 +1,98 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/mesh/mesh_features.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +MeshFeatures::MeshFeatures() + : feature_count_(0), + null_feature_id_(-1), + attribute_index_(-1), + property_table_index_(-1) {} + +void MeshFeatures::Copy(const MeshFeatures &src) { + label_ = src.label_; + feature_count_ = src.feature_count_; + null_feature_id_ = src.null_feature_id_; + attribute_index_ = src.attribute_index_; + texture_map_.Copy(src.texture_map_); + texture_channels_ = src.texture_channels_; + property_table_index_ = src.property_table_index_; +} + +void MeshFeatures::SetLabel(const std::string &label) { label_ = label; } + +const std::string &MeshFeatures::GetLabel() const { return label_; } + +void MeshFeatures::SetFeatureCount(int feature_count) { + feature_count_ = feature_count; +} + +int MeshFeatures::GetFeatureCount() const { return feature_count_; } + +void MeshFeatures::SetNullFeatureId(int null_feature_id) { + null_feature_id_ = null_feature_id; +} + +int MeshFeatures::GetNullFeatureId() const { return null_feature_id_; } + +void MeshFeatures::SetAttributeIndex(int attribute_index) { + attribute_index_ = attribute_index; +} + +int MeshFeatures::GetAttributeIndex() const { return attribute_index_; } + +void MeshFeatures::SetTextureMap(const TextureMap &texture_map) { + texture_map_.Copy(texture_map); +} + +void MeshFeatures::SetTextureMap(Texture *texture, int tex_coord_index) { + texture_map_.SetProperties(TextureMap::GENERIC, tex_coord_index); + texture_map_.SetTexture(texture); +} + +const TextureMap &MeshFeatures::GetTextureMap() const { return texture_map_; } + +TextureMap &MeshFeatures::GetTextureMap() { return texture_map_; } + +void MeshFeatures::SetTextureChannels( + const std::vector &texture_channels) { + texture_channels_ = texture_channels; +} + +const std::vector &MeshFeatures::GetTextureChannels() const { + return texture_channels_; +} + +std::vector &MeshFeatures::GetTextureChannels() { + return texture_channels_; +} + +void MeshFeatures::SetPropertyTableIndex(int property_table_index) { + property_table_index_ = property_table_index; +} + +int MeshFeatures::GetPropertyTableIndex() const { + return property_table_index_; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_features.h b/contrib/draco/src/draco/mesh/mesh_features.h new file mode 100644 index 000000000..af024013f --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_features.h @@ -0,0 +1,93 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_MESH_MESH_FEATURES_H_ +#define DRACO_MESH_MESH_FEATURES_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/texture/texture_library.h" +#include "draco/texture/texture_map.h" + +namespace draco { + +// Describes a mesh feature ID set according to the EXT_mesh_features glTF +// extension. Feature IDs are either associated with geometry vertices or with +// texture pixels and stored in a geometry attribute or in texture channels, +// respectively. Optionally, the feature ID set may be associated with a +// property table defined in the EXT_structural_metadata glTF extension. +class MeshFeatures { + public: + // Creates an empty feature ID set that is associated neither with vertices, + // nor with texture pixels, nor with property tables. + MeshFeatures(); + + // Copies all data from |src| mesh feature ID set. + void Copy(const MeshFeatures &src); + + // Label assigned to this feature ID set. + void SetLabel(const std::string &label); + const std::string &GetLabel() const; + + // The number of unique features in this feature ID set. + void SetFeatureCount(int feature_count); + int GetFeatureCount() const; + + // Non-negative null feature ID value indicating the absence of an associated + // feature. The value of -1 indicates that the null feature ID is not set. + void SetNullFeatureId(int null_feature_id); + int GetNullFeatureId() const; + + // Index of the feature ID vertex attribute, e.g., 5 for an attribute named + // _FEATURE_ID_5, or -1 if the feature ID is not associated with vertices. + void SetAttributeIndex(int attribute_index); + int GetAttributeIndex() const; + + // Feature ID texture map and texture channels containing feature IDs + // associated with texture pixels. Only used when |attribute_index_| is -1. + // The RGBA channels are numbered from 0 to 3. See the glTF extension + // documentation for reconstruction of feature ID from the channel values. + void SetTextureMap(const TextureMap &texture_map); + void SetTextureMap(Texture *texture, int tex_coord_index); + const TextureMap &GetTextureMap() const; + TextureMap &GetTextureMap(); + void SetTextureChannels(const std::vector &texture_channels); + const std::vector &GetTextureChannels() const; + std::vector &GetTextureChannels(); + + // Non-negative index of the property table this feature ID set is associated + // with. Property tables are defined in the EXT_structural_metadata glTF + // extension. The value of -1 indicates that this feature ID set is not + // associated with any property tables. + void SetPropertyTableIndex(int property_table_index); + int GetPropertyTableIndex() const; + + private: + std::string label_; + int feature_count_; + int null_feature_id_; + int attribute_index_; + TextureMap texture_map_; + std::vector texture_channels_; + int property_table_index_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MESH_MESH_FEATURES_H_ diff --git a/contrib/draco/src/draco/mesh/mesh_features_test.cc b/contrib/draco/src/draco/mesh/mesh_features_test.cc new file mode 100644 index 000000000..0e67af2b1 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_features_test.cc @@ -0,0 +1,98 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/mesh/mesh_features.h" + +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/texture/texture_map.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(MeshFeaturesTest, TestDefaults) { + // Test construction of an empty feature ID set. + draco::MeshFeatures mesh_features; + ASSERT_TRUE(mesh_features.GetLabel().empty()); + ASSERT_EQ(mesh_features.GetFeatureCount(), 0); + ASSERT_EQ(mesh_features.GetNullFeatureId(), -1); + ASSERT_EQ(mesh_features.GetAttributeIndex(), -1); + ASSERT_EQ(mesh_features.GetPropertyTableIndex(), -1); + ASSERT_TRUE(mesh_features.GetTextureChannels().empty()); + ASSERT_EQ(mesh_features.GetTextureMap().texture(), nullptr); + ASSERT_EQ(mesh_features.GetTextureMap().type(), draco::TextureMap::GENERIC); +} + +TEST(MeshFeaturesTest, TestSettersAndGetters) { + // Test setter and getter methods of the feature ID set. + draco::MeshFeatures mesh_features; + mesh_features.SetLabel("continent"); + mesh_features.SetFeatureCount(8); + mesh_features.SetNullFeatureId(0); + mesh_features.SetAttributeIndex(2); + mesh_features.SetPropertyTableIndex(10); + std::vector channels = {2, 3}; + mesh_features.SetTextureChannels({2, 3}); + draco::TextureMap texture_map; + texture_map.SetProperties(draco::TextureMap::GENERIC, 1); + std::unique_ptr texture(new draco::Texture()); + texture_map.SetTexture(texture.get()); + mesh_features.SetTextureMap(texture_map); + + // Check that mesh feature set properties can be accessed via getters. + ASSERT_EQ(mesh_features.GetLabel(), "continent"); + ASSERT_EQ(mesh_features.GetFeatureCount(), 8); + ASSERT_EQ(mesh_features.GetNullFeatureId(), 0); + ASSERT_EQ(mesh_features.GetAttributeIndex(), 2); + ASSERT_EQ(mesh_features.GetPropertyTableIndex(), 10); + ASSERT_EQ(mesh_features.GetTextureChannels(), channels); + ASSERT_EQ(mesh_features.GetTextureMap().texture(), texture.get()); + ASSERT_EQ(mesh_features.GetTextureMap().type(), draco::TextureMap::GENERIC); +} + +TEST(MeshFeaturesTest, TestCopy) { + // Test that feature ID set can be copied. + draco::MeshFeatures mesh_features; + mesh_features.SetLabel("continent"); + mesh_features.SetFeatureCount(8); + mesh_features.SetNullFeatureId(0); + mesh_features.SetAttributeIndex(2); + mesh_features.SetPropertyTableIndex(10); + std::vector channels = {2, 3}; + mesh_features.SetTextureChannels({2, 3}); + std::unique_ptr texture(new draco::Texture()); + mesh_features.SetTextureMap(texture.get(), 1); + + // Make a copy. + draco::MeshFeatures copy; + copy.Copy(mesh_features); + + // Check the copy. + ASSERT_EQ(copy.GetLabel(), "continent"); + ASSERT_EQ(copy.GetFeatureCount(), 8); + ASSERT_EQ(copy.GetNullFeatureId(), 0); + ASSERT_EQ(copy.GetAttributeIndex(), 2); + ASSERT_EQ(copy.GetPropertyTableIndex(), 10); + ASSERT_EQ(copy.GetTextureChannels(), channels); + ASSERT_EQ(copy.GetTextureMap().texture(), texture.get()); + ASSERT_EQ(copy.GetTextureMap().type(), draco::TextureMap::GENERIC); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/mesh/mesh_indices.h b/contrib/draco/src/draco/mesh/mesh_indices.h new file mode 100644 index 000000000..5df28d550 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_indices.h @@ -0,0 +1,37 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifdef DRACO_TRANSCODER_SUPPORTED +#ifndef DRACO_MESH_MESH_INDICES_H_ +#define DRACO_MESH_MESH_INDICES_H_ + +#include + +#include + +#include "draco/core/draco_index_type.h" + +namespace draco { + +// Index of a mesh feature ID set. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshFeaturesIndex) + +// Constants denoting invalid indices. +static constexpr MeshFeaturesIndex kInvalidMeshFeaturesIndex( + std::numeric_limits::max()); + +} // namespace draco + +#endif // DRACO_MESH_MESH_INDICES_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_misc_functions.h b/contrib/draco/src/draco/mesh/mesh_misc_functions.h index b450bc80c..0a3bcf497 100644 --- a/contrib/draco/src/draco/mesh/mesh_misc_functions.h +++ b/contrib/draco/src/draco/mesh/mesh_misc_functions.h @@ -67,7 +67,6 @@ inline bool IsCornerOppositeToAttributeSeam(CornerIndex ci, // Interpolates an attribute value on a face using given barycentric // coordinates. InterpolatedVectorT should be a VectorD that corresponds to the // values stored in the attribute. -// TODO(ostava): Find a better place for this. template InterpolatedVectorT ComputeInterpolatedAttributeValueOnMeshFace( const Mesh &mesh, const PointAttribute &attribute, FaceIndex fi, diff --git a/contrib/draco/src/draco/mesh/mesh_splitter.cc b/contrib/draco/src/draco/mesh/mesh_splitter.cc new file mode 100644 index 000000000..ac3c4661c --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_splitter.cc @@ -0,0 +1,451 @@ +// Copyright 2017 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/mesh/mesh_splitter.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/mesh/mesh_utils.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#include "draco/point_cloud/point_cloud_builder.h" + +namespace draco { + +// Helper class that handles splitting of meshes with faces / without faces, +// i.e. point clouds. +template +class MeshSplitterInternal { + public: + struct WorkData : public MeshSplitter::WorkData { + // TriangleSoupMeshBuilder or PointCloudBuilder. + std::vector builders; + }; + + // Computes number of elements (faces or points) for each sub-mesh. + Status InitializeWorkDataNumElements(const Mesh &mesh, int split_attribute_id, + WorkData *work_data) const; + // Initializes a builder for a given sub-mesh. + void InitializeBuilder(int b_index, int num_elements, const Mesh &mesh, + int ignored_attribute_id, WorkData *work_data) const; + // Add all faces or points to the builders. + void AddElementsToBuilder(const Mesh &mesh, + const PointAttribute *split_attribute, + WorkData *work_data) const; + // Builds the meshes from the data accumulated in the builders. + StatusOr BuildMeshes(const Mesh &mesh, + WorkData *work_data) const; +}; + +namespace { + +// Helper functions for copying single element from source |mesh| to a target +// builder |b_index| stored in |work_data|. +void AddElementToBuilder( + int b_index, FaceIndex source_i, FaceIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data); +void AddElementToBuilder( + int b_index, PointIndex source_i, PointIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data); +} // namespace + +MeshSplitter::MeshSplitter() + : preserve_materials_(false), + remove_unused_material_indices_(true), + preserve_mesh_features_(false) {} + +StatusOr MeshSplitter::SplitMesh( + const Mesh &mesh, uint32_t split_attribute_id) { + if (mesh.num_attributes() <= split_attribute_id) { + return Status(Status::DRACO_ERROR, "Invalid attribute id."); + } + if (mesh.num_faces() == 0) { + return SplitMeshInternal(mesh, split_attribute_id); + } else { + return SplitMeshInternal(mesh, split_attribute_id); + } +} + +template +StatusOr MeshSplitter::SplitMeshInternal( + const Mesh &mesh, int split_attribute_id) { + const PointAttribute *const split_attribute = + mesh.attribute(split_attribute_id); + + // Preserve the split attribute only if it is the material attribute and the + // |preserve_materials_| flag is set. Othwerwise the split attribute will get + // discarded. + // TODO(ostava): We may revisit this later and add an option to always + // preserve the split attribute. + const bool preserve_split_attribute = + preserve_materials_ && + split_attribute->attribute_type() == GeometryAttribute::MATERIAL; + + const int num_out_meshes = split_attribute->size(); + MeshSplitterInternal splitter_internal; + typename MeshSplitterInternal::WorkData work_data; + work_data.num_sub_mesh_elements.resize(num_out_meshes, 0); + work_data.split_by_materials = + (split_attribute->attribute_type() == GeometryAttribute::MATERIAL); + + DRACO_RETURN_IF_ERROR(splitter_internal.InitializeWorkDataNumElements( + mesh, split_attribute_id, &work_data)); + + // Create the sub-meshes. + work_data.builders.resize(num_out_meshes); + // Map between attribute ids of the input and output meshes. + work_data.att_id_map.resize(mesh.num_attributes(), -1); + const int ignored_att_id = + (!preserve_split_attribute ? split_attribute_id : -1); + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (work_data.num_sub_mesh_elements[mi] == 0) { + continue; // Empty mesh, don't initialize it. + } + + const int num_elements = work_data.num_sub_mesh_elements[mi]; + splitter_internal.InitializeBuilder(mi, num_elements, mesh, ignored_att_id, + &work_data); + + // Reset the element counter for the sub-mesh. It will be used to keep track + // of number of elements added to the sub-mesh. + work_data.num_sub_mesh_elements[mi] = 0; + } + + splitter_internal.AddElementsToBuilder(mesh, split_attribute, &work_data); + + DRACO_ASSIGN_OR_RETURN(MeshVector out_meshes, + splitter_internal.BuildMeshes(mesh, &work_data)); + return FinalizeMeshes(mesh, work_data, std::move(out_meshes)); +} + +template <> +Status +MeshSplitterInternal::InitializeWorkDataNumElements( + const Mesh &mesh, int split_attribute_id, WorkData *work_data) const { + const PointAttribute *const split_attribute = + mesh.attribute(split_attribute_id); + // Verify that the attribute values are defined "per-face", i.e., all points + // on a face are always mapped to the same attribute value. + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto face = mesh.face(fi); + const AttributeValueIndex avi = split_attribute->mapped_index(face[0]); + for (int c = 1; c < 3; ++c) { + if (split_attribute->mapped_index(face[c]) != avi) { + return Status(Status::DRACO_ERROR, + "Attribute values not consistent on a face."); + } + } + work_data->num_sub_mesh_elements[avi.value()] += 1; + } + return OkStatus(); +} + +template <> +Status MeshSplitterInternal::InitializeWorkDataNumElements( + const Mesh &mesh, int split_attribute_id, WorkData *work_data) const { + const PointAttribute *const split_attribute = + mesh.attribute(split_attribute_id); + // Each point can have a different value. Just accumulate the number of points + // with the same attribute value index. + for (PointIndex pi(0); pi < mesh.num_points(); ++pi) { + const AttributeValueIndex avi = split_attribute->mapped_index(pi); + work_data->num_sub_mesh_elements[avi.value()] += 1; + } + return OkStatus(); +} + +template +void MeshSplitterInternal::InitializeBuilder( + int b_index, int num_elements, const Mesh &mesh, int ignored_attribute_id, + WorkData *work_data) const { + work_data->builders[b_index].Start(num_elements); + + // Add all attributes. + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + if (ai == ignored_attribute_id) { + continue; + } + const GeometryAttribute *const src_att = mesh.attribute(ai); + work_data->att_id_map[ai] = work_data->builders[b_index].AddAttribute( + src_att->attribute_type(), src_att->num_components(), + src_att->data_type()); + } +} + +template <> +void MeshSplitterInternal::AddElementsToBuilder( + const Mesh &mesh, const PointAttribute *split_attribute, + WorkData *work_data) const { + // Go over all faces of the input mesh and add them to the appropriate + // sub-mesh. + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto face = mesh.face(fi); + const int sub_mesh_id = split_attribute->mapped_index(face[0]).value(); + const FaceIndex target_fi(work_data->num_sub_mesh_elements[sub_mesh_id]++); + AddElementToBuilder(sub_mesh_id, fi, target_fi, mesh, work_data); + } +} + +template <> +void MeshSplitterInternal::AddElementsToBuilder( + const Mesh &mesh, const PointAttribute *split_attribute, + WorkData *work_data) const { + // Go over all points of the input mesh and add them to the appropriate + // sub-mesh. + for (PointIndex pi(0); pi < mesh.num_points(); ++pi) { + const int sub_mesh_id = split_attribute->mapped_index(pi).value(); + const PointIndex target_pi(work_data->num_sub_mesh_elements[sub_mesh_id]++); + AddElementToBuilder(sub_mesh_id, pi, target_pi, mesh, work_data); + } +} + +namespace { + +void AddElementToBuilder( + int b_index, FaceIndex source_i, FaceIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data) { + const auto &face = mesh.face(source_i); + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + const PointAttribute *const src_att = mesh.attribute(ai); + const int target_att_id = work_data->att_id_map[ai]; + if (target_att_id == -1) { + continue; + } + // Add value for each corner of the face. + work_data->builders[b_index].SetAttributeValuesForFace( + target_att_id, target_i, src_att->GetAddressOfMappedIndex(face[0]), + src_att->GetAddressOfMappedIndex(face[1]), + src_att->GetAddressOfMappedIndex(face[2])); + } +} + +void AddElementToBuilder( + int b_index, PointIndex source_i, PointIndex target_i, const Mesh &mesh, + MeshSplitterInternal::WorkData *work_data) { + for (int ai = 0; ai < mesh.num_attributes(); ++ai) { + const PointAttribute *const src_att = mesh.attribute(ai); + const int target_att_id = work_data->att_id_map[ai]; + if (target_att_id == -1) { + continue; + } + // Add value for the point |target_i|. + work_data->builders[b_index].SetAttributeValueForPoint( + target_att_id, target_i, src_att->GetAddressOfMappedIndex(source_i)); + } +} + +} // namespace + +template <> +StatusOr +MeshSplitterInternal::BuildMeshes( + const Mesh &mesh, WorkData *work_data) const { + const int num_out_meshes = work_data->builders.size(); + MeshSplitter::MeshVector out_meshes(num_out_meshes); + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (work_data->num_sub_mesh_elements[mi] == 0) { + continue; + } + out_meshes[mi] = work_data->builders[mi].Finalize(); + if (out_meshes[mi] == nullptr) { + continue; + } + } + return out_meshes; +} + +template <> +StatusOr +MeshSplitterInternal::BuildMeshes( + const Mesh &mesh, WorkData *work_data) const { + const int num_out_meshes = work_data->builders.size(); + MeshSplitter::MeshVector out_meshes(num_out_meshes); + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (work_data->num_sub_mesh_elements[mi] == 0) { + continue; + } + // For point clouds, we first build a point cloud and copy it over into + // a draco::Mesh. + std::unique_ptr pc = work_data->builders[mi].Finalize(true); + if (pc == nullptr) { + continue; + } + std::unique_ptr mesh(new Mesh()); + PointCloud *mesh_pc = mesh.get(); + mesh_pc->Copy(*pc); + out_meshes[mi] = std::move(mesh); + } + return out_meshes; +} + +StatusOr MeshSplitter::FinalizeMeshes( + const Mesh &mesh, const WorkData &work_data, MeshVector out_meshes) const { + // Finalize meshes. + const int num_out_meshes = out_meshes.size(); + + // If we are going to preserve mesh features, we will need to update texture + // pointers for all mesh feature textures. Here we store the mapping between + // the old texture pointers and their indices. + std::unordered_map features_texture_to_index_map; + if (preserve_mesh_features_) { + features_texture_to_index_map = + mesh.GetNonMaterialTextureLibrary().ComputeTextureToIndexMap(); + } + + for (int mi = 0; mi < num_out_meshes; ++mi) { + if (out_meshes[mi] == nullptr) { + continue; + } + out_meshes[mi]->SetName(mesh.GetName()); + if (preserve_materials_) { + out_meshes[mi]->GetMaterialLibrary().Copy(mesh.GetMaterialLibrary()); + } + + // Copy metadata of the original mesh to the output meshes. + if (mesh.GetMetadata() != nullptr) { + const GeometryMetadata &metadata = *mesh.GetMetadata(); + out_meshes[mi]->AddMetadata( + std::unique_ptr(new GeometryMetadata(metadata))); + } + + // Copy over attribute unique ids. + for (int att_id = 0; att_id < mesh.num_attributes(); ++att_id) { + const int mapped_att_id = work_data.att_id_map[att_id]; + if (mapped_att_id == -1) { + continue; + } + const PointAttribute *const src_att = mesh.attribute(att_id); + PointAttribute *const dst_att = out_meshes[mi]->attribute(mapped_att_id); + dst_att->set_unique_id(src_att->unique_id()); + } + + // Copy compression settings of the original mesh to the output meshes. + out_meshes[mi]->SetCompressionEnabled(mesh.IsCompressionEnabled()); + out_meshes[mi]->SetCompressionOptions(mesh.GetCompressionOptions()); + + if (preserve_mesh_features_) { + // Copy mesh features from the source |mesh| to the |out_meshes[mi]|. + for (MeshFeaturesIndex mfi(0); mfi < mesh.NumMeshFeatures(); ++mfi) { + if (work_data.split_by_materials) { + // Copy over only those mesh features that were masked to the material + // corresponding to |mi|. + bool is_used = false; + if (mesh.NumMeshFeaturesMaterialMasks(mfi) == 0) { + is_used = true; + } else { + for (int mask_index = 0; + mask_index < mesh.NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + if (mesh.GetMeshFeaturesMaterialMask(mfi, mask_index) == mi) { + is_used = true; + break; + } + } + } + if (!is_used) { + // Ignore this mesh features. + continue; + } + } + // Create a copy of source mesh features. + std::unique_ptr mf(new MeshFeatures()); + mf->Copy(mesh.GetMeshFeatures(mfi)); + const MeshFeaturesIndex new_mfi = + out_meshes[mi]->AddMeshFeatures(std::move(mf)); + if (work_data.split_by_materials && !preserve_materials_) { + // If the input |mesh| was split by materials and we didn't preserve + // the materials, all mesh features must be masked to material 0. + out_meshes[mi]->AddMeshFeaturesMaterialMask(new_mfi, 0); + } else { + // Otherwise mesh features use same masking as the source mesh because + // the material attribute is still present in the split meshes. + // Note that this masking can be later changed in + // RemoveUnusedMaterials() call below. + for (int mask_index = 0; + mask_index < mesh.NumMeshFeaturesMaterialMasks(mfi); + ++mask_index) { + out_meshes[mi]->AddMeshFeaturesMaterialMask( + new_mfi, mesh.GetMeshFeaturesMaterialMask(mfi, mask_index)); + } + } + } + + // Copy over all features textures to the split mesh. + out_meshes[mi]->GetNonMaterialTextureLibrary().Copy( + mesh.GetNonMaterialTextureLibrary()); + + // Update mesh features texture pointers to the new library. + for (MeshFeaturesIndex mfi(0); mfi < out_meshes[mi]->NumMeshFeatures(); + ++mfi) { + Mesh::UpdateMeshFeaturesTexturePointer( + features_texture_to_index_map, + &out_meshes[mi]->GetNonMaterialTextureLibrary(), + &out_meshes[mi]->GetMeshFeatures(mfi)); + } + + // This will remove any mesh features that may not be be actually used + // by this |out_meshes[mi]| (e.g. because corresponding material indices + // were not present in this split mesh). This also removes any unused + // features textures from the non-material texture library. + DRACO_RETURN_IF_ERROR( + MeshUtils::RemoveUnusedMeshFeatures(out_meshes[mi].get())); + } + + // Remove unused materials after we remove mesh features because some of + // the mesh features may have referenced old material indices. + if (preserve_materials_) { + out_meshes[mi]->RemoveUnusedMaterials(remove_unused_material_indices_); + } + + // Copy structural metadata from input mesh to each of the output meshes. + out_meshes[mi]->GetStructuralMetadata().Copy(mesh.GetStructuralMetadata()); + } + return std::move(out_meshes); +} + +StatusOr MeshSplitter::SplitMeshToComponents( + const Mesh &mesh, const MeshConnectedComponents &connected_components) { + // Create the sub-meshes. + const int num_out_meshes = connected_components.NumConnectedComponents(); + MeshSplitterInternal splitter_internal; + typename MeshSplitterInternal::WorkData work_data; + work_data.builders.resize(num_out_meshes); + work_data.num_sub_mesh_elements.resize(num_out_meshes, 0); + work_data.att_id_map.resize(mesh.num_attributes(), -1); + for (int mi = 0; mi < num_out_meshes; ++mi) { + const int num_faces = connected_components.NumConnectedComponentFaces(mi); + work_data.num_sub_mesh_elements[mi] = num_faces; + splitter_internal.InitializeBuilder(mi, num_faces, mesh, -1, &work_data); + } + + // Go over all faces of the input mesh and add them to the appropriate + // sub-mesh. + for (int mi = 0; mi < num_out_meshes; ++mi) { + for (int cfi = 0; cfi < connected_components.NumConnectedComponentFaces(mi); + ++cfi) { + const FaceIndex fi( + connected_components.GetConnectedComponent(mi).faces[cfi]); + const FaceIndex target_fi(cfi); + AddElementToBuilder(mi, fi, target_fi, mesh, &work_data); + } + } + DRACO_ASSIGN_OR_RETURN(auto out_meshes, + splitter_internal.BuildMeshes(mesh, &work_data)); + return FinalizeMeshes(mesh, work_data, std::move(out_meshes)); +} + +} // namespace draco +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_splitter.h b/contrib/draco/src/draco/mesh/mesh_splitter.h new file mode 100644 index 000000000..bf5cd9794 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_splitter.h @@ -0,0 +1,109 @@ +// Copyright 2017 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_MESH_MESH_SPLITTER_H_ +#define DRACO_MESH_MESH_SPLITTER_H_ + +#include +#include + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status_or.h" +#include "draco/mesh/mesh.h" +#include "draco/mesh/mesh_connected_components.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" + +namespace draco { + +// Class that can be used to split a single mesh into multiple sub-meshes +// according to specified criteria. +class MeshSplitter { + public: + typedef std::vector> MeshVector; + MeshSplitter(); + + // Sets a flag that tells the splitter to preserve all materials on the input + // mesh during mesh splitting. When set, the materials used on sub-meshes are + // going to be copied over. Any redundant materials on sub-meshes are going to + // be deleted but material indices may still be preserved depending on the + // SetRemoveUnusedMaterialIndices() flag. + // Default = false. + void SetPreserveMaterials(bool flag) { preserve_materials_ = flag; } + + // Sets a flag that tells the splitter to delete any unused material indices + // on the generated sub-meshes. This option is currently used only when + // SetPreserveMaterials() was set to true. If this option is set to false, the + // material indices of the MATERIAL attribute will be the same as in the + // source mesh. If the flag is true, then the unused material indices will be + // removed and they may no longer correspond to the source mesh. Note that + // when this flag is false, any unused materials would be replaced with empty + // (default) materials. + // Default = true. + void SetRemoveUnusedMaterialIndices(bool flag) { + remove_unused_material_indices_ = flag; + } + + // Sets a flag that tells the splitter to preserve all mesh features on the + // input mesh during mesh splitting. When set, the mesh features used on + // sub-meshes are going to be copied over. Any redundant mesh features on + // sub-meshes are going to be deleted. + // Default = false. + void SetPreserveMeshFeatures(bool flag) { preserve_mesh_features_ = flag; } + + // Splits the input |mesh| according to attribute values stored in the + // specified attribute. If the |mesh| contains faces, the attribute values + // need to be defined per-face, that is, all points attached to a single face + // must share the same attribute value. Meshes without faces are treated as + // point clouds and the attribute values can be defined per-point. Each + // attribute value (AttributeValueIndex) is mapped to a single output mesh. If + // an AttributeValueIndex is unused, no mesh is created for the given value. + StatusOr SplitMesh(const Mesh &mesh, uint32_t split_attribute_id); + + // Splits the input |mesh| into separate components defined in + // |connected_components|. That is, all faces associated with a given + // component index will be stored in the same mesh. The number of generated + // meshes will correspond to |connected_components.NumConnectedComponents()|. + StatusOr SplitMeshToComponents( + const Mesh &mesh, const MeshConnectedComponents &connected_components); + + private: + struct WorkData { + // Map between attribute ids of the input and output meshes. + std::vector att_id_map; + std::vector num_sub_mesh_elements; + bool split_by_materials = false; + }; + + template + StatusOr SplitMeshInternal(const Mesh &mesh, + int split_attribute_id); + + StatusOr FinalizeMeshes(const Mesh &mesh, + const WorkData &work_data, + MeshVector out_meshes) const; + + bool preserve_materials_; + bool remove_unused_material_indices_; + bool preserve_mesh_features_; + + template + friend class MeshSplitterInternal; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MESH_MESH_SPLITTER_H_ diff --git a/contrib/draco/src/draco/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc b/contrib/draco/src/draco/mesh/mesh_splitter_test.cc similarity index 51% rename from contrib/draco/src/draco/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc rename to contrib/draco/src/draco/mesh/mesh_splitter_test.cc index 29e7ed3ba..7432c4736 100644 --- a/contrib/draco/src/draco/javascript/emscripten/draco_animation_encoder_glue_wrapper.cc +++ b/contrib/draco/src/draco/mesh/mesh_splitter_test.cc @@ -12,14 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// This file is used by emscripten's WebIDL Binder. -// http://kripken.github.io/emscripten-site/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html -#include "draco/attributes/geometry_attribute.h" -#include "draco/attributes/point_attribute.h" -#include "draco/compression/encode.h" -#include "draco/javascript/emscripten/animation_encoder_webidl_wrapper.h" -#include "draco/mesh/mesh.h" -#include "draco/point_cloud/point_cloud.h" +#include "draco/mesh/mesh_splitter.h" -// glue_animation_encoder.cpp is generated by Makefile.emcc build_glue target. -#include "glue_animation_encoder.cpp" +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/core/vector_d.h" +#include "draco/io/mesh_io.h" +#include "draco/mesh/mesh_misc_functions.h" + +namespace {} // namespace +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_stripifier.h b/contrib/draco/src/draco/mesh/mesh_stripifier.h index 262e3c792..8e8d8d9f2 100644 --- a/contrib/draco/src/draco/mesh/mesh_stripifier.h +++ b/contrib/draco/src/draco/mesh/mesh_stripifier.h @@ -71,8 +71,6 @@ class MeshStripifier { mesh_ = &mesh; num_strips_ = 0; num_encoded_faces_ = 0; - // TODO(ostava): We may be able to avoid computing the corner table if we - // already have it stored somewhere. corner_table_ = CreateCornerTableFromPositionAttribute(mesh_); if (corner_table_ == nullptr) { return false; diff --git a/contrib/draco/src/draco/mesh/mesh_test.cc b/contrib/draco/src/draco/mesh/mesh_test.cc new file mode 100644 index 000000000..7cc046a7e --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_test.cc @@ -0,0 +1,644 @@ +// Copyright 2018 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/mesh/mesh.h" + +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/compression/draco_compression_options.h" +#include "draco/material/material_utils.h" +#include "draco/mesh/mesh_are_equivalent.h" +#include "draco/mesh/mesh_features.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/mesh/triangle_soup_mesh_builder.h" +#endif // DRACO_TRANSCODER_SUPPORTED + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED +// Tests naming of a mesh. +TEST(MeshTest, MeshName) { + draco::Mesh mesh; + ASSERT_TRUE(mesh.GetName().empty()); + mesh.SetName("Bob"); + ASSERT_EQ(mesh.GetName(), "Bob"); +} + +// Tests copying of a mesh. +TEST(MeshTest, MeshCopy) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, mesh_copy)); +} + +// Tests that we can copy a mesh to a different mesh that already contains some +// data. +TEST(MeshTest, MeshCopyToExistingMesh) { + const std::unique_ptr mesh_0 = + draco::ReadMeshFromTestFile("cube_att.obj"); + const std::unique_ptr mesh_1 = + draco::ReadMeshFromTestFile("test_nm.obj"); + ASSERT_NE(mesh_0, nullptr); + ASSERT_NE(mesh_1, nullptr); + draco::MeshAreEquivalent eq; + ASSERT_FALSE(eq(*mesh_0, *mesh_1)); + + mesh_1->Copy(*mesh_0); + ASSERT_TRUE(eq(*mesh_0, *mesh_1)); +} + +// Tests that we can remove unused materials from a mesh. +TEST(MeshTest, RemoveUnusedMaterials) { + // Input mesh has 29 materials defined in the source file but only 7 are + // actually used. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("mat_test.obj"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const mat_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(mat_att, nullptr); + ASSERT_EQ(mat_att->size(), 29); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), mat_att->size()); + + // Get materials on all faces. + std::vector face_materials(mesh->num_faces(), + nullptr); + for (draco::FaceIndex fi(0); fi < mesh->num_faces(); ++fi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(mesh->face(fi)[0], &mat_index); + face_materials[fi.value()] = + mesh->GetMaterialLibrary().GetMaterial(mat_index); + } + + mesh->RemoveUnusedMaterials(); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 7); + + // Ensure the material attribute contains material indices in the valid range. + for (draco::AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + ASSERT_LT(mat_index, mesh->GetMaterialLibrary().NumMaterials()); + } + + // Ensure all materials are still the same for all faces. + for (draco::FaceIndex fi(0); fi < mesh->num_faces(); ++fi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(mesh->face(fi)[0], &mat_index); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(mat_index), + face_materials[fi.value()]); + } +} + +TEST(MeshTest, RemoveUnusedMaterialsOnPointClud) { + // Input mesh has 29 materials defined in the source file but only 7 are + // actually used. Same as above test but we remove all faces and treat the + // model as a point cloud. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("mat_test.obj"); + ASSERT_NE(mesh, nullptr); + + // Make it a point cloud. + mesh->SetNumFaces(0); + + const draco::PointAttribute *const mat_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(mat_att, nullptr); + ASSERT_EQ(mat_att->size(), 29); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), mat_att->size()); + + // Get materials on all points. + std::vector point_materials(mesh->num_points(), + nullptr); + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(pi, &mat_index); + point_materials[pi.value()] = + mesh->GetMaterialLibrary().GetMaterial(mat_index); + } + + mesh->RemoveUnusedMaterials(); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 7); + + // Ensure the material attribute contains material indices in the valid range. + for (draco::AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + ASSERT_LT(mat_index, mesh->GetMaterialLibrary().NumMaterials()); + } + + // Ensure all materials are still the same for all points. + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + uint32_t mat_index = 0; + mat_att->GetMappedValue(pi, &mat_index); + ASSERT_EQ(mesh->GetMaterialLibrary().GetMaterial(mat_index), + point_materials[pi.value()]); + } +} + +TEST(MeshTest, RemoveUnusedMaterialsNoIndices) { + // The same as above but we actually want to remove only materials and not + // material indices. Therefore we should end up with the same number of + // materials as source but all unused materials should be "default". + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("mat_test.obj"); + ASSERT_NE(mesh, nullptr); + + const draco::PointAttribute *const mat_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::MATERIAL); + ASSERT_NE(mat_att, nullptr); + ASSERT_EQ(mat_att->size(), 29); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), mat_att->size()); + + // Do not remove unused material indices. + mesh->RemoveUnusedMaterials(false); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 29); + + // Gether which materials were actually used and check that all remaining + // materials are "default". + std::vector is_mat_used(mesh->GetMaterialLibrary().NumMaterials(), + false); + for (draco::AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + is_mat_used[mat_index] = true; + } + + for (int mi = 0; mi < mesh->GetMaterialLibrary().NumMaterials(); ++mi) { + if (!is_mat_used[mi]) { + ASSERT_TRUE(draco::MaterialUtils::AreMaterialsEquivalent( + *mesh->GetMaterialLibrary().GetMaterial(mi), draco::Material())); + } + } +} + +TEST(MeshTest, TestAddNewAttributeWithConnectivity) { + // Tests that we can add new attributes with arbitrary connectivity to an + // existing mesh. + + // Create a simple quad. See corner indices of the quad on the figure below: + // + // *-------* + // |2\3 5| + // | \ | + // | \ | + // | \ | + // | \4| + // |0 1\| + // *-------* + // + draco::TriangleSoupMeshBuilder mb; + mb.Start(2); + mb.AddAttribute(draco::GeometryAttribute::POSITION, 3, draco::DT_FLOAT32); + mb.SetAttributeValuesForFace( + 0, draco::FaceIndex(0), draco::Vector3f(0, 0, 0).data(), + draco::Vector3f(1, 0, 0).data(), draco::Vector3f(1, 1, 0).data()); + mb.SetAttributeValuesForFace( + 0, draco::FaceIndex(1), draco::Vector3f(1, 1, 0).data(), + draco::Vector3f(1, 0, 0).data(), draco::Vector3f(1, 1, 1).data()); + std::unique_ptr mesh = mb.Finalize(); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->num_points(), 4); + ASSERT_EQ(mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION)->size(), + 4); + + // Create a simple attribute that has a constant value on every corner. + std::unique_ptr pa(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, 1 /*One components*/, + draco::DT_UINT8, false, 1); + uint8_t val = 10; + pa->SetAttributeValue(draco::AttributeValueIndex(0), &val); + + // Map all corners to the same value. + draco::IndexTypeVector + corner_to_point(6, draco::AttributeValueIndex(0)); + + // Adding this attribute to the mesh should not increase the number of points. + const int new_att_id_0 = + mesh->AddAttributeWithConnectivity(std::move(pa), corner_to_point); + + ASSERT_EQ(mesh->num_attributes(), 2); + ASSERT_EQ(mesh->num_points(), 4); + + const draco::PointAttribute *const new_att_0 = mesh->attribute(new_att_id_0); + ASSERT_NE(new_att_0, nullptr); + + // All points of the mesh should be mapped to the same attribute value. + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + uint8_t att_val = 0; + new_att_0->GetMappedValue(pi, &att_val); + ASSERT_EQ(att_val, 10); + } + + // Add a new attribute with two values and different connectivity. + pa = std::unique_ptr(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, 1 /*One components*/, + draco::DT_UINT8, false, 2); + val = 11; + pa->SetAttributeValue(draco::AttributeValueIndex(0), &val); + val = 12; + pa->SetAttributeValue(draco::AttributeValueIndex(1), &val); + + // Map all corners to the value index 0 except for corner 1 that is mapped to + // value index 1. This should result in a new point being created on either + // corner 1 or corner 4 (see figure at the beginning of this test). + corner_to_point.assign(6, draco::AttributeValueIndex(0)); + corner_to_point[draco::CornerIndex(1)] = draco::AttributeValueIndex(1); + + const int new_att_id_1 = + mesh->AddAttributeWithConnectivity(std::move(pa), corner_to_point); + + ASSERT_EQ(mesh->num_attributes(), 3); + + // One new point should have been created by adding the new attribute. + ASSERT_EQ(mesh->num_points(), 5); + + const draco::PointAttribute *const new_att_1 = mesh->attribute(new_att_id_1); + ASSERT_NE(new_att_1, nullptr); + ASSERT_TRUE(mesh->CornerToPointId(1) == draco::PointIndex(4) || + mesh->CornerToPointId(4) == draco::PointIndex(4)); + + new_att_1->GetMappedValue(mesh->CornerToPointId(1), &val); + ASSERT_EQ(val, 12); + + new_att_1->GetMappedValue(mesh->CornerToPointId(4), &val); + ASSERT_EQ(val, 11); + + // Ensure the attribute values of the remaining attributes are well defined + // on the new point. + draco::Vector3f pos; + mesh->attribute(0)->GetMappedValue(draco::PointIndex(4), &pos[0]); + ASSERT_EQ(pos, draco::Vector3f(1, 0, 0)); + + new_att_0->GetMappedValue(draco::PointIndex(4), &val); + ASSERT_EQ(val, 10); + + new_att_0->GetMappedValue(mesh->CornerToPointId(1), &val); + ASSERT_EQ(val, 10); + new_att_0->GetMappedValue(mesh->CornerToPointId(4), &val); + ASSERT_EQ(val, 10); +} + +TEST(MeshTest, TestAddNewAttributeWithConnectivityWithIsolatedVertices) { + // Tests that we can add a new attribute with connectivity to a mesh that + // contains isolated vertices. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("isolated_vertices.ply"); + ASSERT_NE(mesh, nullptr); + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att, nullptr); + ASSERT_TRUE(pos_att->is_mapping_identity()); + ASSERT_EQ(pos_att->size(), 5); + ASSERT_EQ(mesh->num_points(), 5); + ASSERT_EQ(mesh->num_faces(), 2); + + // Add a new attribute with two values (one for each face). + auto pa = std::unique_ptr(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, 1 /*One component*/, + draco::DT_UINT8, false, 2); + uint8_t val = 11; + pa->SetAttributeValue(draco::AttributeValueIndex(0), &val); + val = 12; + pa->SetAttributeValue(draco::AttributeValueIndex(1), &val); + + draco::IndexTypeVector + corner_to_point(6, draco::AttributeValueIndex(0)); + // All corners on the second face are mapped to the value 1. + for (draco::CornerIndex ci(3); ci < 6; ++ci) { + corner_to_point[ci] = draco::AttributeValueIndex(1); + } + + const draco::PointAttribute *const pa_raw = pa.get(); + mesh->AddAttributeWithConnectivity(std::move(pa), corner_to_point); + + // Two new point should have been added. + ASSERT_EQ(mesh->num_points(), 7); + + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + ASSERT_NE(pa_raw->mapped_index(pi), draco::kInvalidAttributeValueIndex); + ASSERT_NE(pos_att->mapped_index(pi), draco::kInvalidAttributeValueIndex); + } +} + +TEST(MeshTest, TestAddPerVertexAttribute) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + + ASSERT_NE(mesh, nullptr); + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION); + ASSERT_NE(pos_att, nullptr); + + // The input mesh should have 8 spatial vertices. + ASSERT_EQ(pos_att->size(), 8); + + // Add a new scalar attribute where each value corresponds to the position + // value index (vertex). + std::unique_ptr pa(new draco::PointAttribute()); + pa->Init(draco::GeometryAttribute::GENERIC, /* scalar */ 1, draco::DT_FLOAT32, + false, /* one value per position value */ 8); + + // Set the value for the new attribute. + for (draco::AttributeValueIndex avi(0); avi < 8; ++avi) { + const float att_value = avi.value(); + pa->SetAttributeValue(avi, &att_value); + } + + // Add the attribute to the existing mesh. + const int new_att_id = mesh->AddPerVertexAttribute(std::move(pa)); + ASSERT_NE(new_att_id, -1); + + // Make sure all the attribute values are set correctly for every point of the + // mesh. + for (draco::PointIndex pi(0); pi < mesh->num_points(); ++pi) { + const draco::AttributeValueIndex pos_avi = pos_att->mapped_index(pi); + const draco::AttributeValueIndex new_att_avi = + mesh->attribute(new_att_id)->mapped_index(pi); + ASSERT_EQ(pos_avi, new_att_avi); + + float new_att_value; + mesh->attribute(new_att_id)->GetValue(new_att_avi, &new_att_value); + ASSERT_EQ(new_att_value, new_att_avi.value()); + } +} + +TEST(MeshTest, TestRemovalOfIsolatedPoints) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("isolated_vertices.ply"); + + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + + ASSERT_EQ(mesh_copy.num_points(), 5); + mesh_copy.RemoveIsolatedPoints(); + ASSERT_EQ(mesh_copy.num_points(), 4); + + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, mesh_copy)); +} + +TEST(MeshTest, TestCompressionSettings) { + // Tests compression settings of a mesh. + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Check that compression is disabled and compression settings are default. + ASSERT_FALSE(mesh->IsCompressionEnabled()); + const draco::DracoCompressionOptions default_compression_options; + ASSERT_EQ(mesh->GetCompressionOptions(), default_compression_options); + + // Check that compression options can be set without enabling compression. + draco::DracoCompressionOptions compression_options; + compression_options.quantization_bits_normal = 12; + mesh->SetCompressionOptions(compression_options); + ASSERT_EQ(mesh->GetCompressionOptions(), compression_options); + ASSERT_FALSE(mesh->IsCompressionEnabled()); + + // Check that compression can be enabled. + mesh->SetCompressionEnabled(true); + ASSERT_TRUE(mesh->IsCompressionEnabled()); + + // Check that individual compression options can be updated. + mesh->GetCompressionOptions().compression_level++; + mesh->GetCompressionOptions().compression_level--; + + // Check that compression settings can be copied. + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + ASSERT_TRUE(mesh_copy.IsCompressionEnabled()); + ASSERT_EQ(mesh_copy.GetCompressionOptions(), compression_options); +} + +// Tests adding and removing of mesh features to a mesh. +TEST(MeshTest, TestMeshFeatures) { + // Create a mesh with two feature ID sets. + draco::Mesh mesh; + ASSERT_EQ(mesh.NumMeshFeatures(), 0); + std::unique_ptr oceans(new draco::MeshFeatures()); + std::unique_ptr continents(new draco::MeshFeatures()); + oceans->SetLabel("oceans"); + continents->SetLabel("continents"); + const draco::MeshFeaturesIndex index_0 = + mesh.AddMeshFeatures(std::move(oceans)); + const draco::MeshFeaturesIndex index_1 = + mesh.AddMeshFeatures(std::move(continents)); + ASSERT_EQ(index_0, draco::MeshFeaturesIndex(0)); + ASSERT_EQ(index_1, draco::MeshFeaturesIndex(1)); + + // Check that the mesh has two feature ID sets. + ASSERT_EQ(mesh.NumMeshFeatures(), 2); + ASSERT_EQ(mesh.GetMeshFeatures(index_0).GetLabel(), "oceans"); + ASSERT_EQ(mesh.GetMeshFeatures(index_1).GetLabel(), "continents"); + + // Remove one feature ID set and check the remaining feature ID set. + mesh.RemoveMeshFeatures(draco::MeshFeaturesIndex(1)); + ASSERT_EQ(mesh.NumMeshFeatures(), 1); + ASSERT_EQ(mesh.GetMeshFeatures(draco::MeshFeaturesIndex(0)).GetLabel(), + "oceans"); + + // Remove the remaining feature ID set and check that no sets remain. + mesh.RemoveMeshFeatures(draco::MeshFeaturesIndex(0)); + ASSERT_EQ(mesh.NumMeshFeatures(), 0); +} + +// Tests copying of a mesh with feature ID sets. +TEST(MeshTest, MeshCopyWithMeshFeatures) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Add two textures to the non-material texture library of the mesh. + std::unique_ptr texture0(new draco::Texture()); + std::unique_ptr texture1(new draco::Texture()); + texture0->Resize(128, 128); + texture1->Resize(256, 256); + texture0->FillImage(draco::RGBA(100, 0, 0, 0)); + texture1->FillImage(draco::RGBA(200, 0, 0, 0)); + draco::TextureLibrary &library = mesh->GetNonMaterialTextureLibrary(); + library.PushTexture(std::move(texture0)); + library.PushTexture(std::move(texture1)); + + // Add feature ID set referring to an attribute. + const draco::MeshFeaturesIndex index_0 = mesh->AddMeshFeatures( + std::unique_ptr(new draco::MeshFeatures())); + mesh->GetMeshFeatures(index_0).SetLabel("planet"); + mesh->GetMeshFeatures(index_0).SetFeatureCount(2); + mesh->GetMeshFeatures(index_0).SetAttributeIndex(1); + + // Add feature ID set referring to texture at index 0. + const draco::MeshFeaturesIndex index_1 = mesh->AddMeshFeatures( + std::unique_ptr(new draco::MeshFeatures())); + mesh->GetMeshFeatures(index_1).SetLabel("continents"); + mesh->GetMeshFeatures(index_1).SetFeatureCount(7); + mesh->GetMeshFeatures(index_1).GetTextureMap().SetTexture( + library.GetTexture(0)); + + // Add feature ID set referring to a texture at index 1. + const draco::MeshFeaturesIndex index_2 = mesh->AddMeshFeatures( + std::unique_ptr(new draco::MeshFeatures())); + mesh->GetMeshFeatures(index_2).SetLabel("oceans"); + mesh->GetMeshFeatures(index_2).SetFeatureCount(5); + mesh->GetMeshFeatures(index_2).GetTextureMap().SetTexture( + library.GetTexture(1)); + + // Check mesh feature ID set texture pointers. + ASSERT_EQ(library.NumTextures(), 2); + ASSERT_EQ(mesh->NumMeshFeatures(), 3); + ASSERT_EQ(mesh->GetMeshFeatures(index_0).GetTextureMap().texture(), nullptr); + ASSERT_EQ(mesh->GetMeshFeatures(index_1).GetTextureMap().texture(), + library.GetTexture(0)); + ASSERT_EQ(mesh->GetMeshFeatures(index_2).GetTextureMap().texture(), + library.GetTexture(1)); + + // Copy the mesh. + draco::Mesh mesh_copy; + mesh_copy.Copy(*mesh); + + // Check that the meshes are equivalent. + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(*mesh, mesh_copy)); + + // Also check that the texture pointers have been updated correctly. + const draco::TextureLibrary &library_copy = + mesh_copy.GetNonMaterialTextureLibrary(); + ASSERT_EQ(library_copy.NumTextures(), 2); + ASSERT_EQ(mesh_copy.NumMeshFeatures(), 3); + ASSERT_EQ(mesh_copy.GetMeshFeatures(index_0).GetTextureMap().texture(), + nullptr); + ASSERT_EQ(mesh_copy.GetMeshFeatures(index_1).GetTextureMap().texture(), + library_copy.GetTexture(0)); + ASSERT_EQ(mesh_copy.GetMeshFeatures(index_2).GetTextureMap().texture(), + library_copy.GetTexture(1)); +} + +// Tests copying of a mesh with structural metadata. +TEST(MeshTest, TestCopyWithStructuralMetadata) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Add structural metadata to the mesh. + draco::PropertyTable::Schema schema; + schema.json.SetString("Data"); + mesh->GetStructuralMetadata().SetPropertyTableSchema(schema); + + // Copy the mesh. + draco::Mesh copy; + copy.Copy(*mesh); + + // Check that the structural metadata has been copied. + ASSERT_EQ( + copy.GetStructuralMetadata().GetPropertyTableSchema().json.GetString(), + "Data"); +} + +// Tests removing of unused materials for a mesh with mesh features. +TEST(MeshTest, RemoveUnusedMaterialsWithMeshFeatures) { + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("BoxesMeta/glTF/BoxesMeta.gltf"); + ASSERT_NE(mesh, nullptr); + + // Input has five mesh features, two associated with material 0 and three with + // material 1. + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(0), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(1), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(2), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(3), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(4), 0), + 1); + + // Remove material 0. + draco::PointAttribute *mat_att = mesh->attribute( + mesh->GetNamedAttributeId(draco::GeometryAttribute::MATERIAL)); + // Map mat value 0 to 1. + uint32_t new_mat_index = 1; + mat_att->SetAttributeValue(draco::AttributeValueIndex(0), &new_mat_index); + + // This should not do anything because we still have the material 0 referenced + // by mesh features 0 and 1. + mesh->RemoveUnusedMaterials(); + + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + + // Now remove unused mesh features (should be 0 and 1). + DRACO_ASSERT_OK(draco::MeshUtils::RemoveUnusedMeshFeatures(mesh.get())); + + ASSERT_EQ(mesh->NumMeshFeatures(), 3); + // All remaining mesh features should be still mapped to material 1. + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(0), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(1), 0), + 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(2), 0), + 1); + + // Now remove the unused materials (0). + mesh->RemoveUnusedMaterials(); + + // Only one material should be remaining and all the mesh features should now + // be mapped to material 0. + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(0), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(1), 0), + 0); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(draco::MeshFeaturesIndex(2), 0), + 0); +} +#endif // DRACO_TRANSCODER_SUPPORTED + +// Test bounding box. +TEST(MeshTest, TestMeshBoundingBox) { + const draco::Vector3f max_pt(1, 1, 1); + const draco::Vector3f min_pt(0, 0, 0); + + const std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr) << "Failed in Loading: " + << "cube_att.obj"; + const draco::BoundingBox bounding_box = mesh->ComputeBoundingBox(); + + EXPECT_EQ(max_pt[0], bounding_box.GetMaxPoint()[0]); + EXPECT_EQ(max_pt[1], bounding_box.GetMaxPoint()[1]); + EXPECT_EQ(max_pt[2], bounding_box.GetMaxPoint()[2]); + + EXPECT_EQ(min_pt[0], bounding_box.GetMinPoint()[0]); + EXPECT_EQ(min_pt[1], bounding_box.GetMinPoint()[1]); + EXPECT_EQ(min_pt[2], bounding_box.GetMinPoint()[2]); +} + +} // namespace diff --git a/contrib/draco/src/draco/mesh/mesh_utils.cc b/contrib/draco/src/draco/mesh/mesh_utils.cc new file mode 100644 index 000000000..0fbe366c1 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_utils.cc @@ -0,0 +1,492 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/mesh/mesh_utils.h" + +#include +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/attributes/attribute_quantization_transform.h" +#include "draco/core/quantization_utils.h" + +namespace draco { + +void MeshUtils::TransformMesh(const Eigen::Matrix4d &transform, Mesh *mesh) { + // Transform positions. + PointAttribute *pos_att = + mesh->attribute(mesh->GetNamedAttributeId(GeometryAttribute::POSITION)); + for (AttributeValueIndex avi(0); avi < pos_att->size(); ++avi) { + Vector3f pos_val; + pos_att->GetValue(avi, &pos_val[0]); + Eigen::Vector4d transformed_val(pos_val[0], pos_val[1], pos_val[2], 1); + transformed_val = transform * transformed_val; + pos_val = + Vector3f(transformed_val[0], transformed_val[1], transformed_val[2]); + pos_att->SetAttributeValue(avi, &pos_val[0]); + } + + // Transform normals and tangents. + PointAttribute *normal_att = nullptr; + PointAttribute *tangent_att = nullptr; + if (mesh->NumNamedAttributes(GeometryAttribute::NORMAL) > 0) { + normal_att = + mesh->attribute(mesh->GetNamedAttributeId(GeometryAttribute::NORMAL)); + } + if (mesh->NumNamedAttributes(GeometryAttribute::TANGENT) > 0) { + tangent_att = + mesh->attribute(mesh->GetNamedAttributeId(GeometryAttribute::TANGENT)); + } + + if (normal_att || tangent_att) { + // Use inverse-transpose matrix to transform normals and tangents. + Eigen::Matrix3d it_transform = transform.block<3, 3>(0, 0); + + it_transform = it_transform.inverse().transpose(); + + if (normal_att) { + TransformNormalizedAttribute(it_transform, normal_att); + } + if (tangent_att) { + TransformNormalizedAttribute(it_transform, tangent_att); + } + } +} + +namespace { + +// Merges entries from |src_metadata| to |dst_metadata|. Any metadata entries +// with the same names are left unchanged. +void MergeMetadataInternal(const Metadata &src_metadata, + Metadata *dst_metadata) { + const auto &src_entries = src_metadata.entries(); + const auto &dst_entries = dst_metadata->entries(); + for (const auto &it : src_entries) { + if (dst_entries.find(it.first) != dst_entries.end()) { + // Source entry already exists in the target metadata. + continue; + } + // Copy over the entry (entries don't store the data type so binary copy + // is ok). + dst_metadata->AddEntryBinary(it.first, it.second.data()); + } + + // Merge any sub-metadata. + const auto &src_sub_metadata = src_metadata.sub_metadatas(); + const auto &dst_sub_metadata = dst_metadata->sub_metadatas(); + for (const auto &it : src_sub_metadata) { + if (dst_sub_metadata.find(it.first) == dst_sub_metadata.end()) { + // Source sub-metadata doesn't exists in the target metadata, copy it + // over. + std::unique_ptr sub_metadata(new Metadata(*it.second)); + dst_metadata->AddSubMetadata(it.first, std::move(sub_metadata)); + continue; + } + // Merge entries on the sub-metadata. + MergeMetadataInternal(*it.second, dst_metadata->sub_metadata(it.first)); + } +} + +} // namespace + +void MeshUtils::MergeMetadata(const Mesh &src_mesh, Mesh *dst_mesh) { + const auto *src_metadata = src_mesh.GetMetadata(); + if (src_metadata == nullptr) { + return; // Nothing to merge. + } + if (dst_mesh->GetMetadata() == nullptr) { + // Create new metadata for the |dst_mesh|. We do not copy the metadata + // directly because some of the underlying attribute metadata may need to + // be remapped to the format used by |dst_mesh| (e.g. unique ids of the + // attributes may have changed or some attributes may be missing on the + // |dst_mesh|). + std::unique_ptr new_metadata(new GeometryMetadata()); + dst_mesh->AddMetadata(std::move(new_metadata)); + } + auto *dst_metadata = dst_mesh->metadata(); + + // First go over all entries of the geometry part of |src_metadata|. + MergeMetadataInternal(*src_metadata, dst_metadata); + + // Go over attribute metadata. Merges only metadata for attributes that exist + // both on the source and target meshes. Attribute unique ids are remapped + // if needed. + for (int att_type_i = 0; + att_type_i < GeometryAttribute::NAMED_ATTRIBUTES_COUNT; ++att_type_i) { + const GeometryAttribute::Type att_type = + static_cast(att_type_i); + // TODO(ostava): Handle case when the number of attributes of a given type + // does not match. + if (src_mesh.NumNamedAttributes(att_type) != + dst_mesh->NumNamedAttributes(att_type)) { + continue; + } + for (int j = 0; j < src_mesh.NumNamedAttributes(att_type); ++j) { + // First check if we have a metadata for this attribute. + const PointAttribute *const src_att = + src_mesh.GetNamedAttribute(att_type, j); + const auto *src_metadata = + src_mesh.GetMetadata()->GetAttributeMetadataByUniqueId( + src_att->unique_id()); + if (src_metadata == nullptr) { + // No metadata at the source, ignore the attribute. + continue; + } + // Find target attribute corresponding to the source. + const PointAttribute *const dst_att = + dst_mesh->GetNamedAttribute(att_type, j); + if (dst_att == nullptr) { + // No corresponding attribute found, ignore the source metadata. + continue; + } + auto *dst_metadata = + dst_mesh->metadata()->attribute_metadata(dst_att->unique_id()); + if (dst_metadata == nullptr) { + // Copy over the metadata (with remapped attribute unique id). + std::unique_ptr new_metadata( + new AttributeMetadata(*src_metadata)); + new_metadata->set_att_unique_id(dst_att->unique_id()); + dst_mesh->metadata()->AddAttributeMetadata(std::move(new_metadata)); + continue; + } + // Merge metadata entries. + MergeMetadataInternal(*src_metadata, dst_metadata); + } + } +} + +Status MeshUtils::RemoveUnusedMeshFeatures(Mesh *mesh) { + // Unused mesh features are features that are not used by any face / vertex + // of the |mesh|. Currently, each mesh feature can be "masked" for specific + // materials, in which case we need to check whether the mask materials + // are present in the |mesh|. If not, we can remove the mesh features from the + // mesh. + const PointAttribute *const mat_att = + mesh->GetNamedAttribute(GeometryAttribute::MATERIAL); + // Find which materials are used. + std::unordered_set used_materials; + if (mat_att == nullptr) { + // Only material with index 0 is assumed to be used. + used_materials.insert(0); + } else { + for (AttributeValueIndex avi(0); avi < mat_att->size(); ++avi) { + uint32_t mat_index = 0; + mat_att->GetValue(avi, &mat_index); + used_materials.insert(mat_index); + } + } + + std::vector unused_mesh_features; + for (MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + bool is_used = false; + if (mesh->NumMeshFeaturesMaterialMasks(mfi) == 0) { + is_used = true; + } else { + for (int mask_i = 0; mask_i < mesh->NumMeshFeaturesMaterialMasks(mfi); + ++mask_i) { + const int material_index = + mesh->GetMeshFeaturesMaterialMask(mfi, mask_i); + if (used_materials.count(material_index)) { + is_used = true; + break; + } + } + } + if (!is_used) { + unused_mesh_features.push_back(mfi); + } + } + + // Remove the unused mesh features (from back). + for (auto it = unused_mesh_features.rbegin(); + it != unused_mesh_features.rend(); ++it) { + const MeshFeaturesIndex mfi = *it; + mesh->RemoveMeshFeatures(mfi); + } + + // Remove all features textures that are not used anymore. + + // First find which textures are referenced by the mesh features. + std::unordered_set used_textures; + for (MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + const Texture *const texture = + mesh->GetMeshFeatures(mfi).GetTextureMap().texture(); + if (texture) { + used_textures.insert(texture); + } + } + + if (!used_textures.empty() && + mesh->GetNonMaterialTextureLibrary().NumTextures() == 0) { + return ErrorStatus( + "Trying to remove mesh features textures that are not owned by the " + "mesh."); + } + + // Remove all unreferenced textures from the non-material texture library. + for (int ti = mesh->GetNonMaterialTextureLibrary().NumTextures() - 1; ti >= 0; + --ti) { + const Texture *const texture = + mesh->GetNonMaterialTextureLibrary().GetTexture(ti); + if (used_textures.count(texture) == 0) { + mesh->GetNonMaterialTextureLibrary().RemoveTexture(ti); + } + } + return OkStatus(); +} + +bool MeshUtils::FlipTextureUvValues(bool flip_u, bool flip_v, + PointAttribute *att) { + if (att->attribute_type() != GeometryAttribute::TEX_COORD) { + return false; + } + if (att->data_type() != DataType::DT_FLOAT32) { + return false; + } + if (att->num_components() != 2) { + return false; + } + + std::array value; + for (AttributeValueIndex avi(0); avi < att->size(); ++avi) { + if (!att->GetValue(avi, &value)) { + return false; + } + if (flip_u) { + value[0] = 1.0 - value[0]; + } + if (flip_v) { + value[1] = 1.0 - value[1]; + } + att->SetAttributeValue(avi, value.data()); + } + return true; +} + +// TODO(fgalligan): Change att_id to be of type const PointAttribute &. +int MeshUtils::CountDegenerateFaces(const Mesh &mesh, int att_id) { + const PointAttribute *const att = mesh.attribute(att_id); + if (att == nullptr) { + return -1; + } + const int num_components = att->num_components(); + switch (num_components) { + case 2: + return MeshUtils::CountDegenerateFaces(mesh, *att); + case 3: + return MeshUtils::CountDegenerateFaces(mesh, *att); + case 4: + return MeshUtils::CountDegenerateFaces(mesh, *att); + default: + break; + } + return -1; +} + +StatusOr MeshUtils::FindLowestTextureQuantization( + const Mesh &mesh, const PointAttribute &pos_att, int pos_quantization_bits, + const PointAttribute &tex_att, int tex_target_quantization_bits) { + if (tex_target_quantization_bits < 0 || tex_target_quantization_bits >= 30) { + return Status(Status::DRACO_ERROR, + "Target texture quantization is out of range."); + } + // The target quantization is no quantization, so return 0. + if (tex_target_quantization_bits == 0) { + return 0; + } + const uint32_t pos_max_quantized_value = (1 << (pos_quantization_bits)) - 1; + AttributeQuantizationTransform pos_transform; + if (!pos_transform.ComputeParameters(pos_att, pos_quantization_bits)) { + return Status(Status::DRACO_ERROR, + "Failed computing position quantization parameters."); + } + + // Get all degenerate faces for positions. If the model already has + // degenerate faces for positions, but valid faces for texture coordinates, + // those will not count as new degenerate faces for texture coordinates, + // because the faces would not have been rendered anyway. + const std::vector pos_degenerate_faces_sorted = + MeshUtils::ListDegenerateQuantizedFaces( + mesh, pos_att, pos_transform.range(), pos_max_quantized_value, false); + + // Initialize return value to zero signifying that it could not find a + // quantization that did not cause any new degenerate faces. + int lowest_quantization_bits = 0; + int min_quantization_bits = tex_target_quantization_bits; + int max_quantization_bits = 29; + while (true) { + const int curr_quantization_bits = + min_quantization_bits + + (max_quantization_bits - min_quantization_bits) / 2; + AttributeQuantizationTransform transform; + if (!transform.ComputeParameters(tex_att, curr_quantization_bits)) { + return Status(Status::DRACO_ERROR, + "Failed computing texture quantization parameters."); + } + + const uint32_t max_quantized_value = (1 << (curr_quantization_bits)) - 1; + + // Get only new degenerate faces for texture coordinates. If the model + // already has degenerate faces for texture coordinates, we don't want to + // take into account those faces in the source, because those faces would + // not have been rendered correctly anyway. + const std::vector tex_degenerate_faces_sorted = + MeshUtils::ListDegenerateQuantizedFaces( + mesh, tex_att, transform.range(), max_quantized_value, true); + + if (tex_degenerate_faces_sorted.size() <= + pos_degenerate_faces_sorted.size()) { + if (std::includes(pos_degenerate_faces_sorted.begin(), + pos_degenerate_faces_sorted.end(), + tex_degenerate_faces_sorted.begin(), + tex_degenerate_faces_sorted.end())) { + // Degenerate texture coordinate faces are a subset of position + // degenerate faces. + lowest_quantization_bits = curr_quantization_bits; + } + } + + if (lowest_quantization_bits == curr_quantization_bits) { + // The lowest quantization is the current quantization, see if lower + // quantization is possible. + max_quantization_bits = curr_quantization_bits - 1; + } else { + min_quantization_bits = curr_quantization_bits + 1; + } + if (min_quantization_bits > max_quantization_bits) { + break; + } + } + return lowest_quantization_bits; +} + +void MeshUtils::TransformNormalizedAttribute(const Eigen::Matrix3d &transform, + PointAttribute *att) { + for (AttributeValueIndex avi(0); avi < att->size(); ++avi) { + // Store up to 4 component values. + Vector4f val(0, 0, 0, 1); + att->GetValue(avi, &val); + // Ignore the last component during transformation. + Eigen::Vector3d transformed_val(val[0], val[1], val[2]); + transformed_val = transform * transformed_val; + transformed_val = transformed_val.normalized(); + // Last component is passed to the transformed value. + val = Vector4f(transformed_val[0], transformed_val[1], transformed_val[2], + val[3]); + + // Set the value to the attribute. Note that in case the attribute is using + // fewer than 4 components, the 4th component is going to be ignored. + att->SetAttributeValue(avi, &val[0]); + } +} + +template +int MeshUtils::CountDegenerateFaces(const Mesh &mesh, + const PointAttribute &att) { + if (att.data_type() != DataType::DT_FLOAT32) { + return -1; + } + std::array values; + int degenerate_values = 0; + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto &face = mesh.face(fi); + for (int c = 0; c < 3; ++c) { + att.GetMappedValue(face[c], &values[c][0]); + } + if (values[0] == values[1] || values[0] == values[2] || + values[1] == values[2]) { + degenerate_values++; + } + } + return degenerate_values; +} + +std::vector MeshUtils::ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only) { + const int num_components = att.num_components(); + switch (num_components) { + case 2: + return MeshUtils::ListDegenerateQuantizedFaces>( + mesh, att, range, max_quantized_value, quantized_degenerate_only); + case 3: + return MeshUtils::ListDegenerateQuantizedFaces>( + mesh, att, range, max_quantized_value, quantized_degenerate_only); + case 4: + return MeshUtils::ListDegenerateQuantizedFaces>( + mesh, att, range, max_quantized_value, quantized_degenerate_only); + default: + break; + } + return std::vector(); +} + +template +std::vector MeshUtils::ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only) { + std::array values; + std::array quantized_values; + + Quantizer quantizer; + quantizer.Init(range, max_quantized_value); + std::vector degenerate_faces; + + for (FaceIndex fi(0); fi < mesh.num_faces(); ++fi) { + const auto &face = mesh.face(fi); + for (int c = 0; c < 3; ++c) { + att.GetMappedValue(face[c], &values[c][0]); + for (int i = 0; i < att_components_t::dimension; ++i) { + quantized_values[c][i] = quantizer.QuantizeFloat(values[c][i]); + } + } + + if (quantized_degenerate_only && + (values[0] == values[1] || values[0] == values[2] || + values[1] == values[2])) { + continue; + } + if (quantized_values[0] == quantized_values[1] || + quantized_values[0] == quantized_values[2] || + quantized_values[1] == quantized_values[2]) { + degenerate_faces.push_back(fi); + } + } + return degenerate_faces; +} + +bool MeshUtils::HasAutoGeneratedTangents(const Mesh &mesh) { + const int tangent_att_id = + mesh.GetNamedAttributeId(draco::GeometryAttribute::TANGENT); + if (tangent_att_id == -1) { + return false; + } + const auto metadata = mesh.GetAttributeMetadataByAttributeId(tangent_att_id); + if (metadata) { + int is_auto_generated = 0; + if (metadata->GetEntryInt("auto_generated", &is_auto_generated) && + is_auto_generated == 1) { + return true; + } + } + return false; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/mesh_utils.h b/contrib/draco/src/draco/mesh/mesh_utils.h new file mode 100644 index 000000000..e17dfd8ed --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_utils.h @@ -0,0 +1,102 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_MESH_MESH_UTILS_H_ +#define DRACO_MESH_MESH_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "Eigen/Geometry" +#include "draco/core/status_or.h" +#include "draco/mesh/mesh.h" + +namespace draco { + +// Helper class containing various utilities operating on draco::Mesh. +// TODO(ostava): Move scattered functions in this folder here (e.g. corner table +// construction). +class MeshUtils { + public: + // Transforms |mesh| using the |transform| matrix. The mesh is transformed + // in-place. + static void TransformMesh(const Eigen::Matrix4d &transform, Mesh *mesh); + + // Merges metadata from |src_mesh| to |dst_mesh|. Any metadata with the same + // names are left unchanged. + static void MergeMetadata(const Mesh &src_mesh, Mesh *dst_mesh); + + // Removes unused MeshFeatures from |mesh|. If the |mesh| contains any mesh + // feature textures, the textures must be owned by the |mesh| otherwise an + // error is returned. + static Status RemoveUnusedMeshFeatures(Mesh *mesh); + + // Flips the UV values of |att|. + static bool FlipTextureUvValues(bool flip_u, bool flip_v, + PointAttribute *att); + + // Counts the number of degenerate faces in |mesh| for attribute |att_id|. + // Returns < 0 if counting of degenerate faces is not supported for |att_id|. + static int CountDegenerateFaces(const Mesh &mesh, int att_id); + + // Searches for the lowest texture quantization bits for |tex_att| that does + // not introduce any new texture coordinate degenerate faces. The range for + // the search is |tex_target_quantization_bits| - 29, inclusive. The function + // does not count texture coordinate degenerate faces already in the source. + // Nor does it count any new texture coordinate degenerate faces that are a + // subset of new position degenerate faces created from the quantization of + // |pos_att| using |pos_quantization_bits|. Returns the lowest quantization + // bits within the specified range or zero signifying that it could not find a + // quantization that did not cause any new degenerate faces. + static StatusOr FindLowestTextureQuantization( + const Mesh &mesh, const PointAttribute &pos_att, + int pos_quantization_bits, const PointAttribute &tex_att, + int tex_target_quantization_bits); + + // Helper function that checks whether a mesh has auto-generated tangents. + // See go/tangents_and_draco_simplifier. + static bool HasAutoGeneratedTangents(const Mesh &mesh); + + private: + static void TransformNormalizedAttribute(const Eigen::Matrix3d &transform, + PointAttribute *att); + + template + static int CountDegenerateFaces(const Mesh &mesh, const PointAttribute &att); + + // Returns a sorted list of degenerate faces for |att|. |att| must use |mesh| + // for its connectivity. |range| and |max_quantized_value| are the values + // passed into the quantizer. |quantized_degenerate_only|, is true will only + // include degenerate faces caused by the quantization. Otherwise all + // degenerate faces will be included, those made by the quantization and those + // already in the source. + static std::vector ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only); + + // Returns a sorted list of degenerate faces for |att|. |att_components_t| is + // the component count for |att| as a VectorD. E.g. Vector2f, Vector3f, or + // Vector4f. |quantized_components_t| is the quantized component count for + // |att| as a VectorD. E.g. VectorD, VectorD, or + // VectorD. + template + static std::vector ListDegenerateQuantizedFaces( + const Mesh &mesh, const PointAttribute &att, float range, + uint32_t max_quantized_value, bool quantized_degenerate_only); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_MESH_MESH_UTILS_H_ diff --git a/contrib/draco/src/draco/mesh/mesh_utils_test.cc b/contrib/draco/src/draco/mesh/mesh_utils_test.cc new file mode 100644 index 000000000..022669cb0 --- /dev/null +++ b/contrib/draco/src/draco/mesh/mesh_utils_test.cc @@ -0,0 +1,391 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/mesh/mesh_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +// Compare normal vector rotated by |angle| around the x-axis. +void CompareRotatedNormals(const draco::Mesh &mesh_0, const draco::Mesh &mesh_1, + float angle) { + const draco::PointAttribute *const norm_att_0 = + mesh_0.GetNamedAttribute(draco::GeometryAttribute::NORMAL); + const draco::PointAttribute *const norm_att_1 = + mesh_1.GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_EQ(norm_att_0->size(), norm_att_1->size()); + for (draco::AttributeValueIndex avi(0); avi < norm_att_0->size(); ++avi) { + Eigen::Vector3f norm_0, norm_1; + norm_att_0->GetValue(avi, norm_0.data()); + norm_att_1->GetValue(avi, norm_1.data()); + + // Project the normals into yz plane + norm_0[0] = 0.f; + norm_1[0] = 0.f; + + if (norm_0.squaredNorm() < 1e-6f) { + // Normal pointing towards X. Make sure the rotated normal is about the + // same. + ASSERT_NEAR(norm_1.squaredNorm(), 0.f, 1e-6f); + continue; + } + + // Ensure the angle between the normals is as expected. + norm_0.normalize(); + norm_1.normalize(); + const float norm_angle = + std::atan2(norm_0.cross(norm_1).norm(), norm_0.dot(norm_1)); + ASSERT_NEAR(std::abs(norm_angle), angle, 1e-6f); + } +} + +TEST(MeshUtilsTest, TestTransform) { + auto mesh = draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + draco::Mesh transformed_mesh; + transformed_mesh.Copy(*mesh); + Eigen::Matrix4d transform = Eigen::Matrix4d::Identity(); + draco::MeshUtils::TransformMesh(transform, &transformed_mesh); + + // Rotate the mesh by 45 deg around the x-axis. + transform.block<3, 3>(0, 0) = + Eigen::Quaterniond( + Eigen::AngleAxisd(M_PI / 4.f, Eigen::Vector3d::UnitX())) + .normalized() + .toRotationMatrix(); + draco::MeshUtils::TransformMesh(transform, &transformed_mesh); + CompareRotatedNormals(*mesh, transformed_mesh, M_PI / 4.f); + + // Now rotate the cube back. + transform.block<3, 3>(0, 0) = + Eigen::Quaterniond( + Eigen::AngleAxisd(-M_PI / 4.f, Eigen::Vector3d::UnitX())) + .normalized() + .toRotationMatrix(); + + draco::MeshUtils::TransformMesh(transform, &transformed_mesh); + CompareRotatedNormals(*mesh, transformed_mesh, 0.f); +} + +TEST(MeshUtilsTest, TestTextureUvFlips) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + ASSERT_NE(mesh, nullptr); + + // Check that FlipTextureUvValues() only works on texture coordinates. + draco::PointAttribute *att = mesh->attribute(0); + ASSERT_EQ(att->attribute_type(), draco::GeometryAttribute::POSITION); + ASSERT_FALSE(draco::MeshUtils::FlipTextureUvValues(false, true, att)); + + att = mesh->attribute(1); + ASSERT_EQ(att->attribute_type(), draco::GeometryAttribute::TEX_COORD); + + // Get the values and flip the V values. + std::vector> check_uv_values; + check_uv_values.resize(att->size()); + for (draco::AttributeValueIndex avi(0); avi < att->size(); ++avi) { + att->GetValue(avi, &check_uv_values[avi.value()]); + check_uv_values[avi.value()][1] = 1.0 - check_uv_values[avi.value()][1]; + } + + ASSERT_TRUE(draco::MeshUtils::FlipTextureUvValues(false, true, att)); + + std::array value; + for (draco::AttributeValueIndex avi(0); avi < att->size(); ++avi) { + att->GetValue(avi, &value); + ASSERT_EQ(value[0], check_uv_values[avi.value()][0]); + ASSERT_EQ(value[1], check_uv_values[avi.value()][1]); + } + + // Flip the U values. + for (int i = 0; i < check_uv_values.size(); ++i) { + check_uv_values[i][0] = 1.0 - check_uv_values[i][0]; + } + + ASSERT_TRUE(draco::MeshUtils::FlipTextureUvValues(true, false, att)); + + for (draco::AttributeValueIndex avi(0); avi < att->size(); ++avi) { + att->GetValue(avi, &value); + ASSERT_EQ(value[0], check_uv_values[avi.value()][0]); + ASSERT_EQ(value[1], check_uv_values[avi.value()][1]); + } +} + +// Tests counting degenerate values for positions and texture coordinates for +// both scene and mesh. +TEST(MeshUtilsTest, CountDegenerateValuesLantern) { + int degenerate_positions_scene = 0; + int degenerate_tex_coords_scene = 0; + std::unique_ptr scene = + draco::ReadSceneFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(scene, nullptr); + + for (int mgi = 0; mgi < scene->NumMeshGroups(); ++mgi) { + const draco::MeshGroup *const mesh_group = + scene->GetMeshGroup(draco::MeshGroupIndex(mgi)); + ASSERT_NE(mesh_group, nullptr); + + for (int mi = 0; mi < mesh_group->NumMeshInstances(); ++mi) { + const draco::MeshIndex mesh_index = + mesh_group->GetMeshInstance(mi).mesh_index; + const draco::Mesh &m = scene->GetMesh(mesh_index); + + for (int i = 0; i < m.num_attributes(); ++i) { + const draco::PointAttribute *const att = m.attribute(i); + ASSERT_NE(att, nullptr); + + if (att->attribute_type() == draco::GeometryAttribute::Type::POSITION) { + degenerate_positions_scene += + draco::MeshUtils::CountDegenerateFaces(m, i); + } else if (att->attribute_type() == + draco::GeometryAttribute::Type::TEX_COORD) { + degenerate_tex_coords_scene += + draco::MeshUtils::CountDegenerateFaces(m, i); + } + } + } + } + EXPECT_EQ(degenerate_positions_scene, 0); + EXPECT_EQ(degenerate_tex_coords_scene, 2); + + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(mesh, nullptr); + for (int i = 0; i < mesh->num_attributes(); ++i) { + const draco::PointAttribute *const att = mesh->attribute(i); + ASSERT_NE(att, nullptr); + if (att->attribute_type() == draco::GeometryAttribute::Type::POSITION) { + EXPECT_EQ(draco::MeshUtils::CountDegenerateFaces(*mesh, i), + degenerate_positions_scene); + } else if (att->attribute_type() == + draco::GeometryAttribute::Type::TEX_COORD) { + EXPECT_EQ(draco::MeshUtils::CountDegenerateFaces(*mesh, i), + degenerate_tex_coords_scene); + } + } +} + +// Tests finding the lowest quantization bits for the texture coordinate in a +// mesh. +TEST(MeshUtilsTest, FindLowsetTextureQuantizationLanternMesh) { + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(mesh, nullptr); + + const int pos_quantization_bits = 11; + const draco::PointAttribute *const pos_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::Type::POSITION, 0); + ASSERT_NE(pos_att, nullptr); + + const draco::PointAttribute *const tex_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::Type::TEX_COORD, 0); + ASSERT_NE(tex_att, nullptr); + + // Tests target no quantization returns no quantization. + const int target_no_quantization_bits = 0; + DRACO_ASSIGN_OR_ASSERT(const int no_quantization_bits, + draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, + target_no_quantization_bits)); + ASSERT_EQ(no_quantization_bits, 0); + + // Test failures. + const int out_of_range_low = -1; + const auto statusor_low = draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, out_of_range_low); + ASSERT_FALSE(statusor_low.ok()); + + const int out_of_range_high = 30; + const auto statusor_high = draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, out_of_range_high); + ASSERT_FALSE(statusor_high.ok()); + + // Tests finding the lowest quantization bits for the texture coordinate. + const int target_bits = 6; + DRACO_ASSIGN_OR_ASSERT( + const int lowest_bits, + draco::MeshUtils::FindLowestTextureQuantization( + *mesh, *pos_att, pos_quantization_bits, *tex_att, target_bits)); + ASSERT_EQ(lowest_bits, 14); +} + +// Tests finding the lowest quantization bits for the texture coordinates for +// the three meshes in the scene. +TEST(MeshUtilsTest, FindLowsetTextureQuantizationLanternScene) { + std::unique_ptr scene = + draco::ReadSceneFromTestFile("Lantern/glTF/Lantern.gltf"); + ASSERT_NE(scene, nullptr); + + const std::vector expected_mesh_quantization_bits{11, 8, 14}; + for (int mi = 0; mi < scene->NumMeshes(); ++mi) { + const draco::Mesh &mesh = scene->GetMesh(draco::MeshIndex(mi)); + + const int pos_quantization_bits = 11; + const draco::PointAttribute *const pos_att = + mesh.GetNamedAttribute(draco::GeometryAttribute::Type::POSITION, 0); + ASSERT_NE(pos_att, nullptr); + + const draco::PointAttribute *const tex_att = + mesh.GetNamedAttribute(draco::GeometryAttribute::Type::TEX_COORD, 0); + ASSERT_NE(tex_att, nullptr); + + const int target_bits = 8; + DRACO_ASSIGN_OR_ASSERT( + const int lowest_bits, + draco::MeshUtils::FindLowestTextureQuantization( + mesh, *pos_att, pos_quantization_bits, *tex_att, target_bits)); + ASSERT_EQ(lowest_bits, expected_mesh_quantization_bits[mi]); + } +} + +TEST(MeshUtilsTest, CheckAutoGeneratedTangents) { + // Test verifies that MeshUtils::HasAutoGeneratedTangents works as intended. + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("sphere_no_tangents.gltf"); + ASSERT_NE(mesh, nullptr); + + ASSERT_TRUE(draco::MeshUtils::HasAutoGeneratedTangents(*mesh)); +} + +TEST(MeshUtilsTest, CheckMergeMetadata) { + // Test verifies that we can merge metadata using MeshUtils::MergeMetadata(). + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("sphere_no_tangents.gltf"); + ASSERT_NE(mesh, nullptr); + + std::unique_ptr other_mesh = + draco::ReadMeshFromTestFile("cube_att.obj"); + + ASSERT_NE(mesh->GetMetadata(), nullptr); + // One attribute metadata (for the tangent attribute) and no other entries. + ASSERT_EQ(mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_EQ(mesh->GetMetadata()->num_entries(), 0); + + // No metadata at the other attribute. + ASSERT_EQ(other_mesh->GetMetadata(), nullptr); + + // First try to merge |other_mesh| metadata to |mesh|. This shouldn't do + // anything. + draco::MeshUtils::MergeMetadata(*other_mesh, mesh.get()); + ASSERT_EQ(mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_EQ(mesh->GetMetadata()->num_entries(), 0); + + // Merge |mesh| metadata to |other_mesh|. This will create empty metadata but + // not any attribute metadata because |other_mesh| doesn't have the tangent + // attribute. + draco::MeshUtils::MergeMetadata(*mesh, other_mesh.get()); + ASSERT_NE(other_mesh->GetMetadata(), nullptr); + ASSERT_EQ(other_mesh->GetMetadata()->attribute_metadatas().size(), 0); + ASSERT_EQ(other_mesh->GetMetadata()->num_entries(), 0); + ASSERT_FALSE(draco::MeshUtils::HasAutoGeneratedTangents(*other_mesh)); + + // Add dummy tangent attribute to the |other_mesh|. + std::unique_ptr tang_att(new draco::PointAttribute()); + draco::PointAttribute *const tang_att_ptr = tang_att.get(); + tang_att->set_attribute_type(draco::GeometryAttribute::TANGENT); + other_mesh->AddAttribute(std::move(tang_att)); + + // Merge |mesh| metadata to |other_mesh|. This time the tangent metadata + // should be copied over. + draco::MeshUtils::MergeMetadata(*mesh, other_mesh.get()); + ASSERT_NE(other_mesh->GetMetadata(), nullptr); + ASSERT_EQ(other_mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_EQ(other_mesh->GetMetadata()->num_entries(), 0); + ASSERT_NE(other_mesh->GetMetadata()->GetAttributeMetadataByUniqueId( + tang_att_ptr->unique_id()), + nullptr); + ASSERT_TRUE(draco::MeshUtils::HasAutoGeneratedTangents(*other_mesh)); + + // Now add some entries to the geometry metadata and merge again. + mesh->metadata()->AddEntryInt("test_int_0", 0); + mesh->metadata()->AddEntryInt("test_int_1", 1); + mesh->metadata()->AddEntryInt("test_int_shared", 2); + other_mesh->metadata()->AddEntryInt("test_int_shared", 3); + + // "test_int_0" and "test_int_1" should be copied over while + // "test_entry_shared" should stay unchanged. + draco::MeshUtils::MergeMetadata(*mesh, other_mesh.get()); + ASSERT_NE(other_mesh->GetMetadata(), nullptr); + // Attribute metadata should stay unchanged. + ASSERT_EQ(other_mesh->GetMetadata()->attribute_metadatas().size(), 1); + ASSERT_NE(other_mesh->GetMetadata()->GetAttributeMetadataByUniqueId( + tang_att_ptr->unique_id()), + nullptr); + ASSERT_EQ(other_mesh->GetMetadata() + ->GetAttributeMetadataByUniqueId(tang_att_ptr->unique_id()) + ->num_entries(), + 1); + + // Check the geometry metadata entries. + ASSERT_EQ(other_mesh->GetMetadata()->num_entries(), 3); + int metadata_value; + ASSERT_TRUE( + other_mesh->GetMetadata()->GetEntryInt("test_int_0", &metadata_value)); + ASSERT_EQ(metadata_value, 0); + ASSERT_TRUE( + other_mesh->GetMetadata()->GetEntryInt("test_int_1", &metadata_value)); + ASSERT_EQ(metadata_value, 1); + + // The shared entry should have an unchanged value. + ASSERT_TRUE(other_mesh->GetMetadata()->GetEntryInt("test_int_shared", + &metadata_value)); + ASSERT_EQ(metadata_value, 3); +} + +TEST(MeshUtilsTest, RemoveUnusedMeshFeatures) { + // Test verifies that MeshUtils::RemoveUnusedMeshFeatures works as intended. + std::unique_ptr mesh = + draco::ReadMeshFromTestFile("BoxesMeta/glTF/BoxesMeta.gltf"); + ASSERT_NE(mesh, nullptr); + + // The input mesh should have five mesh features and two features textures. + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // All of those features and textures should be used so calling the method + // below shouldn't do anything. + draco::MeshUtils::RemoveUnusedMeshFeatures(mesh.get()); + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 2); + + // Now remove material 1 that is mapped to first two mesh features. + draco::PointAttribute *mat_att = mesh->attribute( + mesh->GetNamedAttributeId(draco::GeometryAttribute::MATERIAL)); + + // This basically remaps all faces from material 1 to material 0. + uint32_t mat_index = 0; + mat_att->SetAttributeValue(draco::AttributeValueIndex(1), &mat_index); + + // Try to remove the mesh features again. + draco::MeshUtils::RemoveUnusedMeshFeatures(mesh.get()); + + // Three of the mesh features should have been removed as well as one mesh + // features texture. + ASSERT_EQ(mesh->NumMeshFeatures(), 2); + ASSERT_EQ(mesh->GetNonMaterialTextureLibrary().NumTextures(), 1); + + // Ensure the remaining mesh features are mapped to the correct material. + for (draco::MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + ASSERT_EQ(mesh->NumMeshFeaturesMaterialMasks(mfi), 1); + ASSERT_EQ(mesh->GetMeshFeaturesMaterialMask(mfi, 0), 0); + } +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc index 60b0c50b8..2af94a052 100644 --- a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc +++ b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.cc @@ -23,11 +23,23 @@ void TriangleSoupMeshBuilder::Start(int num_faces) { attribute_element_types_.clear(); } +#ifdef DRACO_TRANSCODER_SUPPORTED +void TriangleSoupMeshBuilder::SetName(const std::string &name) { + mesh_->SetName(name); +} +#endif // DRACO_TRANSCODER_SUPPORTED + int TriangleSoupMeshBuilder::AddAttribute( GeometryAttribute::Type attribute_type, int8_t num_components, DataType data_type) { + return AddAttribute(attribute_type, num_components, data_type, false); +} + +int TriangleSoupMeshBuilder::AddAttribute( + GeometryAttribute::Type attribute_type, int8_t num_components, + DataType data_type, bool normalized) { GeometryAttribute va; - va.Init(attribute_type, nullptr, num_components, data_type, false, + va.Init(attribute_type, nullptr, num_components, data_type, normalized, DataTypeLength(data_type) * num_components, 0); attribute_element_types_.push_back(-1); return mesh_->AddAttribute(va, true, mesh_->num_points()); @@ -41,8 +53,6 @@ void TriangleSoupMeshBuilder::SetAttributeValuesForFace( att->SetAttributeValue(AttributeValueIndex(start_index), corner_value_0); att->SetAttributeValue(AttributeValueIndex(start_index + 1), corner_value_1); att->SetAttributeValue(AttributeValueIndex(start_index + 2), corner_value_2); - // TODO(ostava): The below code should be called only for one attribute. - // It will work OK even for multiple attributes, but it's redundant. mesh_->SetFace(face_id, {{PointIndex(start_index), PointIndex(start_index + 1), PointIndex(start_index + 2)}}); diff --git a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h index 89466e1d8..503fe84c5 100644 --- a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h +++ b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder.h @@ -15,7 +15,14 @@ #ifndef DRACO_MESH_TRIANGLE_SOUP_MESH_BUILDER_H_ #define DRACO_MESH_TRIANGLE_SOUP_MESH_BUILDER_H_ +#include +#include + #include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" +#endif #include "draco/mesh/mesh.h" namespace draco { @@ -25,15 +32,25 @@ namespace draco { // deduplicated. class TriangleSoupMeshBuilder { public: + // Index type of the inserted element. + typedef FaceIndex ElementIndex; + // Starts mesh building for a given number of faces. // TODO(ostava): Currently it's necessary to select the correct number of // faces upfront. This should be generalized, but it will require us to // rewrite our attribute resizing functions. void Start(int num_faces); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Sets mesh name. + void SetName(const std::string &name); +#endif // DRACO_TRANSCODER_SUPPORTED + // Adds an empty attribute to the mesh. Returns the new attribute's id. int AddAttribute(GeometryAttribute::Type attribute_type, int8_t num_components, DataType data_type); + int AddAttribute(GeometryAttribute::Type attribute_type, + int8_t num_components, DataType data_type, bool normalized); // Sets values for a given attribute on all corners of a given face. void SetAttributeValuesForFace(int att_id, FaceIndex face_id, @@ -41,12 +58,34 @@ class TriangleSoupMeshBuilder { const void *corner_value_1, const void *corner_value_2); +#ifdef DRACO_TRANSCODER_SUPPORTED + // Converts input values of type T into internal representation used by + // |att_id|. Each input value needs to have |input_num_components| entries. + template + Status ConvertAndSetAttributeValuesForFace(int att_id, FaceIndex face_id, + int input_num_components, + const T *corner_value_0, + const T *corner_value_1, + const T *corner_value_2); +#endif + // Sets value for a per-face attribute. If all faces of a given attribute are // set with this method, the attribute will be marked as per-face, otherwise // it will be marked as per-corner attribute. void SetPerFaceAttributeValueForFace(int att_id, FaceIndex face_id, const void *value); + // Add metadata. + void AddMetadata(std::unique_ptr metadata) { + mesh_->AddMetadata(std::move(metadata)); + } + + // Add metadata for an attribute. + void AddAttributeMetadata(int32_t att_id, + std::unique_ptr metadata) { + mesh_->AddAttributeMetadata(att_id, std::move(metadata)); + } + // Finalizes the mesh or returns nullptr on error. // Once this function is called, the builder becomes invalid and cannot be // used until the method Start() is called again. @@ -58,6 +97,30 @@ class TriangleSoupMeshBuilder { std::unique_ptr mesh_; }; +#ifdef DRACO_TRANSCODER_SUPPORTED +template +Status TriangleSoupMeshBuilder::ConvertAndSetAttributeValuesForFace( + int att_id, FaceIndex face_id, int input_num_components, + const T *corner_value_0, const T *corner_value_1, const T *corner_value_2) { + const int start_index = 3 * face_id.value(); + PointAttribute *const att = mesh_->attribute(att_id); + DRACO_RETURN_IF_ERROR( + att->ConvertAndSetAttributeValue(AttributeValueIndex(start_index + 0), + input_num_components, corner_value_0)); + DRACO_RETURN_IF_ERROR( + att->ConvertAndSetAttributeValue(AttributeValueIndex(start_index + 1), + input_num_components, corner_value_1)); + DRACO_RETURN_IF_ERROR( + att->ConvertAndSetAttributeValue(AttributeValueIndex(start_index + 2), + input_num_components, corner_value_2)); + mesh_->SetFace(face_id, + {{PointIndex(start_index), PointIndex(start_index + 1), + PointIndex(start_index + 2)}}); + attribute_element_types_[att_id] = MESH_CORNER_ATTRIBUTE; + return OkStatus(); +} +#endif // DRACO_TRANSCODER_SUPPORTED + } // namespace draco #endif // DRACO_MESH_TRIANGLE_SOUP_MESH_BUILDER_H_ diff --git a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc index 171f8fe24..b23641760 100644 --- a/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc +++ b/contrib/draco/src/draco/mesh/triangle_soup_mesh_builder_test.cc @@ -14,7 +14,11 @@ // #include "draco/mesh/triangle_soup_mesh_builder.h" +#include +#include + #include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" #include "draco/core/vector_d.h" namespace draco { @@ -26,6 +30,9 @@ TEST_F(TriangleSoupMeshBuilderTest, CubeTest) { // of the provided triangle soup data. TriangleSoupMeshBuilder mb; mb.Start(12); +#ifdef DRACO_TRANSCODER_SUPPORTED + mb.SetName("Cube"); +#endif const int pos_att_id = mb.AddAttribute(GeometryAttribute::POSITION, 3, DT_FLOAT32); // clang-format off @@ -92,6 +99,9 @@ TEST_F(TriangleSoupMeshBuilderTest, CubeTest) { std::unique_ptr mesh = mb.Finalize(); ASSERT_NE(mesh, nullptr) << "Failed to build the cube mesh."; +#ifdef DRACO_TRANSCODER_SUPPORTED + EXPECT_EQ(mesh->GetName(), "Cube"); +#endif EXPECT_EQ(mesh->num_points(), 8) << "Unexpected number of vertices."; EXPECT_EQ(mesh->num_faces(), 12) << "Unexpected number of faces."; } @@ -139,7 +149,7 @@ TEST_F(TriangleSoupMeshBuilderTest, TestPerFaceAttribs) { Vector3f(0.f, 1.f, 0.f).data(), Vector3f(1.f, 1.f, 0.f).data(), Vector3f(0.f, 1.f, 1.f).data()); - mb.SetPerFaceAttributeValueForFace(gen_att_id, FaceIndex(4), &bool_false);; + mb.SetPerFaceAttributeValueForFace(gen_att_id, FaceIndex(4), &bool_false); mb.SetAttributeValuesForFace(pos_att_id, FaceIndex(5), Vector3f(0.f, 1.f, 1.f).data(), @@ -189,9 +199,69 @@ TEST_F(TriangleSoupMeshBuilderTest, TestPerFaceAttribs) { std::unique_ptr mesh = mb.Finalize(); ASSERT_NE(mesh, nullptr) << "Failed to build the cube mesh."; +#ifdef DRACO_TRANSCODER_SUPPORTED + EXPECT_TRUE(mesh->GetName().empty()); +#endif EXPECT_EQ(mesh->num_faces(), 12) << "Unexpected number of faces."; EXPECT_EQ(mesh->GetAttributeElementType(gen_att_id), MESH_FACE_ATTRIBUTE) << "Unexpected attribute element type."; } +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST_F(TriangleSoupMeshBuilderTest, NormalizedColor) { + // This tests, verifies that the mesh builder constructs a valid model with + // normalized integer colors using floating points as input. + TriangleSoupMeshBuilder mb; + mb.Start(2); + const int pos_att_id = + mb.AddAttribute(GeometryAttribute::POSITION, 3, DT_FLOAT32); + const int color_att_id = + mb.AddAttribute(GeometryAttribute::COLOR, 3, DT_UINT8, true); + + mb.SetAttributeValuesForFace( + pos_att_id, FaceIndex(0), Vector3f(0.f, 0.f, 0.f).data(), + Vector3f(1.f, 0.f, 0.f).data(), Vector3f(0.f, 1.f, 0.f).data()); + DRACO_ASSERT_OK(mb.ConvertAndSetAttributeValuesForFace( + color_att_id, FaceIndex(0), 4, Vector4f(0.f, 0.f, 0.f, 1.f).data(), + Vector4f(1.f, 1.f, 1.f, 1.f).data(), + Vector4f(0.5f, 0.5f, 0.5f, 1.f).data())); + mb.SetAttributeValuesForFace( + pos_att_id, FaceIndex(1), Vector3f(0.f, 1.f, 0.f).data(), + Vector3f(1.f, 0.f, 0.f).data(), Vector3f(1.f, 1.f, 0.f).data()); + + DRACO_ASSERT_OK(mb.ConvertAndSetAttributeValuesForFace( + color_att_id, FaceIndex(1), 4, Vector4f(0.5f, 0.5f, 0.5f, 1.f).data(), + Vector4f(1.f, 1.f, 1.f, 1.f).data(), + Vector4f(0.25f, 0.0f, 1.f, 1.f).data())); + + std::unique_ptr mesh = mb.Finalize(); + ASSERT_NE(mesh, nullptr) << "Failed to build the test mesh."; + + EXPECT_EQ(mesh->num_points(), 4) << "Unexpected number of vertices."; + EXPECT_EQ(mesh->num_faces(), 2) << "Unexpected number of faces."; + + const auto *col_att = + mesh->GetNamedAttribute(draco::GeometryAttribute::COLOR); + ASSERT_NE(col_att, nullptr) << "Missing color attribute."; + ASSERT_EQ(col_att->size(), 4); + + // All colors should be in range 0-255. + uint8_t max_val = 0, min_val = 255; + for (draco::AttributeValueIndex avi(0); avi < col_att->size(); ++avi) { + VectorD cval; + col_att->GetValue(avi, &cval); + const uint8_t max = cval.MaxCoeff(); + const uint8_t min = cval.MinCoeff(); + if (max > max_val) { + max_val = max; + } + if (min < min_val) { + min_val = min; + } + } + ASSERT_EQ(max_val, 255); + ASSERT_EQ(min_val, 0); +} +#endif + } // namespace draco diff --git a/contrib/draco/src/draco/metadata/geometry_metadata.cc b/contrib/draco/src/draco/metadata/geometry_metadata.cc index b83898140..b6a882c0b 100644 --- a/contrib/draco/src/draco/metadata/geometry_metadata.cc +++ b/contrib/draco/src/draco/metadata/geometry_metadata.cc @@ -18,6 +18,19 @@ namespace draco { +AttributeMetadata::AttributeMetadata(const AttributeMetadata &metadata) + : Metadata(metadata) { + att_unique_id_ = metadata.att_unique_id_; +} + +GeometryMetadata::GeometryMetadata(const GeometryMetadata &metadata) + : Metadata(metadata) { + for (size_t i = 0; i < metadata.att_metadatas_.size(); ++i) { + att_metadatas_.push_back(std::unique_ptr( + new AttributeMetadata(*metadata.att_metadatas_[i]))); + } +} + const AttributeMetadata *GeometryMetadata::GetAttributeMetadataByStringEntry( const std::string &entry_name, const std::string &entry_value) const { for (auto &&att_metadata : att_metadatas_) { @@ -35,7 +48,7 @@ const AttributeMetadata *GeometryMetadata::GetAttributeMetadataByStringEntry( bool GeometryMetadata::AddAttributeMetadata( std::unique_ptr att_metadata) { - if (!att_metadata.get()) { + if (!att_metadata) { return false; } att_metadatas_.push_back(std::move(att_metadata)); diff --git a/contrib/draco/src/draco/metadata/geometry_metadata.h b/contrib/draco/src/draco/metadata/geometry_metadata.h index ec7ecb9ee..531bdef25 100644 --- a/contrib/draco/src/draco/metadata/geometry_metadata.h +++ b/contrib/draco/src/draco/metadata/geometry_metadata.h @@ -25,6 +25,7 @@ namespace draco { class AttributeMetadata : public Metadata { public: AttributeMetadata() : att_unique_id_(0) {} + AttributeMetadata(const AttributeMetadata &metadata); explicit AttributeMetadata(const Metadata &metadata) : Metadata(metadata), att_unique_id_(0) {} @@ -57,6 +58,7 @@ struct AttributeMetadataHasher { class GeometryMetadata : public Metadata { public: GeometryMetadata() {} + GeometryMetadata(const GeometryMetadata &metadata); explicit GeometryMetadata(const Metadata &metadata) : Metadata(metadata) {} const AttributeMetadata *GetAttributeMetadataByStringEntry( diff --git a/contrib/draco/src/draco/metadata/metadata.cc b/contrib/draco/src/draco/metadata/metadata.cc index 9141907ed..51b4e93a3 100644 --- a/contrib/draco/src/draco/metadata/metadata.cc +++ b/contrib/draco/src/draco/metadata/metadata.cc @@ -122,6 +122,14 @@ const Metadata *Metadata::GetSubMetadata(const std::string &name) const { return sub_ptr->second.get(); } +Metadata *Metadata::sub_metadata(const std::string &name) { + auto sub_ptr = sub_metadatas_.find(name); + if (sub_ptr == sub_metadatas_.end()) { + return nullptr; + } + return sub_ptr->second.get(); +} + void Metadata::RemoveEntry(const std::string &name) { // Actually just remove "name", no need to check if it exists. auto entry_ptr = entries_.find(name); diff --git a/contrib/draco/src/draco/metadata/metadata.h b/contrib/draco/src/draco/metadata/metadata.h index 56d05e46a..12c1ba974 100644 --- a/contrib/draco/src/draco/metadata/metadata.h +++ b/contrib/draco/src/draco/metadata/metadata.h @@ -147,6 +147,7 @@ class Metadata { bool AddSubMetadata(const std::string &name, std::unique_ptr sub_metadata); const Metadata *GetSubMetadata(const std::string &name) const; + Metadata *sub_metadata(const std::string &name); void RemoveEntry(const std::string &name); diff --git a/contrib/draco/src/draco/metadata/metadata_decoder.cc b/contrib/draco/src/draco/metadata/metadata_decoder.cc index a8e66f854..6468e3207 100644 --- a/contrib/draco/src/draco/metadata/metadata_decoder.cc +++ b/contrib/draco/src/draco/metadata/metadata_decoder.cc @@ -59,18 +59,25 @@ bool MetadataDecoder::DecodeGeometryMetadata(DecoderBuffer *in_buffer, } bool MetadataDecoder::DecodeMetadata(Metadata *metadata) { - struct MetadataPair { + // Limit metadata nesting depth to avoid stack overflow in destructor. + constexpr int kMaxSubmetadataLevel = 1000; + + struct MetadataTuple { Metadata *parent_metadata; Metadata *decoded_metadata; + int level; }; - std::vector metadata_stack; - metadata_stack.push_back({nullptr, metadata}); + std::vector metadata_stack; + metadata_stack.push_back({nullptr, metadata, 0}); while (!metadata_stack.empty()) { - const MetadataPair mp = metadata_stack.back(); + const MetadataTuple mp = metadata_stack.back(); metadata_stack.pop_back(); metadata = mp.decoded_metadata; if (mp.parent_metadata != nullptr) { + if (mp.level > kMaxSubmetadataLevel) { + return false; + } std::string sub_metadata_name; if (!DecodeName(&sub_metadata_name)) { return false; @@ -105,7 +112,8 @@ bool MetadataDecoder::DecodeMetadata(Metadata *metadata) { return false; } for (uint32_t i = 0; i < num_sub_metadata; ++i) { - metadata_stack.push_back({metadata, nullptr}); + metadata_stack.push_back( + {metadata, nullptr, mp.parent_metadata ? mp.level + 1 : mp.level}); } } return true; @@ -123,6 +131,9 @@ bool MetadataDecoder::DecodeEntry(Metadata *metadata) { if (data_size == 0) { return false; } + if (data_size > buffer_->remaining_size()) { + return false; + } std::vector entry_value(data_size); if (!buffer_->Decode(&entry_value[0], data_size)) { return false; diff --git a/contrib/draco/src/draco/metadata/metadata_test.cc b/contrib/draco/src/draco/metadata/metadata_test.cc index cf7ae6eee..03104e03e 100644 --- a/contrib/draco/src/draco/metadata/metadata_test.cc +++ b/contrib/draco/src/draco/metadata/metadata_test.cc @@ -104,12 +104,16 @@ TEST_F(MetadataTest, TestNestedMetadata) { sub_metadata->AddEntryInt("int", 100); metadata.AddSubMetadata("sub0", std::move(sub_metadata)); - const auto sub_metadata_ptr = metadata.GetSubMetadata("sub0"); + const auto sub_metadata_ptr = metadata.sub_metadata("sub0"); ASSERT_NE(sub_metadata_ptr, nullptr); int32_t int_value = 0; ASSERT_TRUE(sub_metadata_ptr->GetEntryInt("int", &int_value)); ASSERT_EQ(int_value, 100); + + sub_metadata_ptr->AddEntryInt("new_entry", 20); + ASSERT_TRUE(sub_metadata_ptr->GetEntryInt("new_entry", &int_value)); + ASSERT_EQ(int_value, 20); } TEST_F(MetadataTest, TestHardCopyMetadata) { diff --git a/contrib/draco/src/draco/metadata/property_table.cc b/contrib/draco/src/draco/metadata/property_table.cc new file mode 100644 index 000000000..c6a5fd984 --- /dev/null +++ b/contrib/draco/src/draco/metadata/property_table.cc @@ -0,0 +1,183 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/metadata/property_table.h" + +#include +#include +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +bool PropertyTable::Schema::Object::operator==(const Object& other) const { + if (type_ != other.type_ || name_ != other.name_) { + return false; + } + switch (type_) { + case OBJECT: + if (objects_.size() != other.objects_.size()) { + return false; + } + for (int i = 0; i < objects_.size(); ++i) { + if (objects_[i] != other.objects_[i]) { + return false; + } + } + break; + case ARRAY: + if (array_.size() != other.array_.size()) { + return false; + } + for (int i = 0; i < array_.size(); ++i) { + if (array_[i] != other.array_[i]) { + return false; + } + } + break; + case STRING: + return string_ == other.string_; + case INTEGER: + return integer_ == other.integer_; + case BOOLEAN: + return boolean_ == other.boolean_; + } + return true; +} + +void PropertyTable::Schema::Object::Copy(const Object& src) { + name_ = src.name_; + type_ = src.type_; + objects_.reserve(src.objects_.size()); + for (const Object& obj : src.objects_) { + objects_.emplace_back(); + objects_.back().Copy(obj); + } + array_.reserve(src.array_.size()); + for (const Object& obj : src.array_) { + array_.emplace_back(); + array_.back().Copy(obj); + } + string_ = src.string_; + integer_ = src.integer_; + boolean_ = src.boolean_; +} + +PropertyTable::Property::Property() {} + +bool PropertyTable::Property::Data::operator==(const Data& other) const { + return data == other.data && target == other.target; +} + +bool PropertyTable::Property::Offsets::operator==(const Offsets& other) const { + return data == other.data && type == other.type; +} + +bool PropertyTable::Property::operator==(const Property& other) const { + return name_ == other.name_ && data_ == other.data_ && + array_offsets_ == other.array_offsets_ && + string_offsets_ == other.string_offsets_; +} + +void PropertyTable::Property::Copy(const Property& src) { + name_ = src.name_; + data_ = src.data_; + array_offsets_ = src.array_offsets_; + string_offsets_ = src.string_offsets_; +} + +void PropertyTable::Property::SetName(const std::string& name) { name_ = name; } +const std::string& PropertyTable::Property::GetName() const { return name_; } + +PropertyTable::Property::Data& PropertyTable::Property::GetData() { + return data_; +} +const PropertyTable::Property::Data& PropertyTable::Property::GetData() const { + return data_; +} + +const PropertyTable::Property::Offsets& +PropertyTable::Property::GetArrayOffsets() const { + return array_offsets_; +} +PropertyTable::Property::Offsets& PropertyTable::Property::GetArrayOffsets() { + return array_offsets_; +} + +const PropertyTable::Property::Offsets& +PropertyTable::Property::GetStringOffsets() const { + return string_offsets_; +} +PropertyTable::Property::Offsets& PropertyTable::Property::GetStringOffsets() { + return string_offsets_; +} + +PropertyTable::PropertyTable() : count_(0) {} + +bool PropertyTable::operator==(const PropertyTable& other) const { + if (name_ != other.name_ || class_ != other.class_ || + count_ != other.count_ || + properties_.size() != other.properties_.size()) { + return false; + } + for (int i = 0; i < properties_.size(); ++i) { + if (*properties_[i] != *other.properties_[i]) { + return false; + } + } + return true; +} + +void PropertyTable::Copy(const PropertyTable& src) { + name_ = src.name_; + class_ = src.class_; + count_ = src.count_; + properties_.clear(); + properties_.reserve(src.properties_.size()); + for (int i = 0; i < src.properties_.size(); ++i) { + std::unique_ptr property(new Property()); + property->Copy(src.GetProperty(i)); + properties_.push_back(std::move(property)); + } +} + +void PropertyTable::SetName(const std::string& value) { name_ = value; } +const std::string& PropertyTable::GetName() const { return name_; } + +void PropertyTable::SetClass(const std::string& value) { class_ = value; } +const std::string& PropertyTable::GetClass() const { return class_; } + +void PropertyTable::SetCount(int count) { count_ = count; } +int PropertyTable::GetCount() const { return count_; } + +int PropertyTable::AddProperty(std::unique_ptr property) { + properties_.push_back(std::move(property)); + return properties_.size() - 1; +} +int PropertyTable::NumProperties() const { return properties_.size(); } +const PropertyTable::Property& PropertyTable::GetProperty(int index) const { + return *properties_[index]; +} +PropertyTable::Property& PropertyTable::GetProperty(int index) { + return *properties_[index]; +} +void PropertyTable::RemoveProperty(int index) { + properties_.erase(properties_.begin() + index); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/metadata/property_table.h b/contrib/draco/src/draco/metadata/property_table.h new file mode 100644 index 000000000..41efb0163 --- /dev/null +++ b/contrib/draco/src/draco/metadata/property_table.h @@ -0,0 +1,243 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_METADATA_PROPERTY_TABLE_H_ +#define DRACO_METADATA_PROPERTY_TABLE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include +#include +#include + +namespace draco { + +// Describes a property table as defined in the EXT_structural_metadata glTF +// extension, including property table schema and table properties (columns). +class PropertyTable { + public: + // Describes property table schema in the form of a JSON object. + struct Schema { + // JSON object of the schema. + // TODO(vytyaz): Consider using a third_party/json library. Currently there + // is a conflict between Filament's assert_invariant() macro and JSON + // library's assert_invariant() method that causes compile errors in Draco + // visualization library. + class Object { + public: + enum Type { OBJECT, ARRAY, STRING, INTEGER, BOOLEAN }; + + // Constructors. + Object() : Object("") {} + explicit Object(const std::string& name) + : name_(name), type_(OBJECT), integer_(0), boolean_(false) {} + Object(const std::string& name, const std::string& value) : Object(name) { + SetString(value); + } + Object(const std::string& name, const char* value) : Object(name) { + SetString(value); + } + Object(const std::string& name, int value) : Object(name) { + SetInteger(value); + } + Object(const std::string& name, bool value) : Object(name) { + SetBoolean(value); + } + + // Methods for comparing two objects. + bool operator==(const Object& other) const; + bool operator!=(const Object& other) const { return !(*this == other); } + + // Method for copying the object. + void Copy(const Object& src); + + // Methods for getting object name and type. + const std::string& GetName() const { return name_; } + Type GetType() const { return type_; } + + // Methods for getting object value. + const std::vector& GetObjects() const { return objects_; } + const std::vector& GetArray() const { return array_; } + const std::string& GetString() const { return string_; } + int GetInteger() const { return integer_; } + bool GetBoolean() const { return boolean_; } + + // Methods for setting object value. + std::vector& SetObjects() { + type_ = OBJECT; + return objects_; + } + std::vector& SetArray() { + type_ = ARRAY; + return array_; + } + void SetString(const std::string& value) { + type_ = STRING; + string_ = value; + } + void SetInteger(int value) { + type_ = INTEGER; + integer_ = value; + } + void SetBoolean(bool value) { + type_ = BOOLEAN; + boolean_ = value; + } + + private: + std::string name_; + Type type_; + std::vector objects_; + std::vector array_; + std::string string_; + int integer_; + bool boolean_; + }; + + // Valid schema top-level JSON object name is "schema". + Schema() : json("schema") {} + + // Methods for comparing two schemas. + bool operator==(const Schema& other) const { return json == other.json; } + bool operator!=(const Schema& other) const { return !(*this == other); } + + // Valid schema top-level JSON object is required to have child objects. + bool Empty() const { return json.GetObjects().empty(); } + + // Top-level JSON object of the schema. + Object json; + }; + + // Describes a property (column) of a property table. + class Property { + public: + // Describes glTF buffer view data. + struct Data { + // Methods for comparing two data objects. + bool operator==(const Data& other) const; + bool operator!=(const Data& other) const { return !(*this == other); } + + // Buffer view data. + std::vector data; + + // Data target corresponds to the target property of the glTF bufferView + // object and classifies the type or nature of the data. + int target = 0; + }; + + // Describes offsets of the entries in property data when the data + // represents an array of strings or an array of variable-length number + // arrays. + struct Offsets { + // Methods for comparing two offsets. + bool operator==(const Offsets& other) const; + bool operator!=(const Offsets& other) const { return !(*this == other); } + + // Data containing the offset entries. + Data data; + + // Data type of the offset entries. + std::string type; + }; + + // Creates an empty property. + Property(); + + // Methods for comparing two properties. + bool operator==(const Property& other) const; + bool operator!=(const Property& other) const { return !(*this == other); } + + // Copies all data from |src| property. + void Copy(const Property& src); + + // Name of this property. + void SetName(const std::string& name); + const std::string& GetName() const; + + // Property data stores one table column worth of data. For example, when + // the data of type UINT8 is [11, 22] then the property values are 11 and 22 + // for the first and second table rows. See EXT_structural_metadata glTF + // extension documentation for more details. + Data& GetData(); + const Data& GetData() const; + + // Array offsets are used when property data contains a variable-length + // number arrays. For example, when the data is [0, 1, 2, 3, 4] and the + // array offsets are [0, 2, 5] for a two-row table, then the property value + // arrays are [0, 1] and [2, 3, 4] for the first and second table rows, + // respectively. See EXT_structural_metadata glTF extension documentation + // for more details. + const Offsets& GetArrayOffsets() const; + Offsets& GetArrayOffsets(); + + // String offsets are used when property data contains strings. For example, + // when the data is "SeaLand" and the array offsets are [0, 3, 7] for a + // two-row table, then the property strings are "Sea" and "Land" for the + // first and second table rows, respectively. See EXT_structural_metadata + // glTF extension documentation for more details. + const Offsets& GetStringOffsets() const; + Offsets& GetStringOffsets(); + + private: + std::string name_; + Data data_; + Offsets array_offsets_; + Offsets string_offsets_; + // TODO(vytyaz): Support property value modifiers min, max, offset, scale. + }; + + // Creates an empty property table. + PropertyTable(); + + // Methods for comparing two property tables. + bool operator==(const PropertyTable& other) const; + bool operator!=(const PropertyTable& other) const { + return !(*this == other); + } + + // Copies all data from |src| property table. + void Copy(const PropertyTable& src); + + // Name of this property table. + void SetName(const std::string& value); + const std::string& GetName() const; + + // Class of this property table. + void SetClass(const std::string& value); + const std::string& GetClass() const; + + // Number of rows in this property table. + void SetCount(int count); + int GetCount() const; + + // Table properties (columns). + int AddProperty(std::unique_ptr property); + int NumProperties() const; + const Property& GetProperty(int index) const; + Property& GetProperty(int index); + void RemoveProperty(int index); + + private: + std::string name_; + std::string class_; + int count_; + std::vector> properties_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_METADATA_PROPERTY_TABLE_H_ diff --git a/contrib/draco/src/draco/metadata/property_table_test.cc b/contrib/draco/src/draco/metadata/property_table_test.cc new file mode 100644 index 000000000..4d5ee2d2c --- /dev/null +++ b/contrib/draco/src/draco/metadata/property_table_test.cc @@ -0,0 +1,624 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/metadata/property_table.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(PropertyTableTest, TestPropertyDataDefaults) { + // Test construction of an empty property data. + draco::PropertyTable::Property::Data data; + ASSERT_TRUE(data.data.empty()); + ASSERT_EQ(data.target, 0); +} + +TEST(PropertyTableTest, TestPropertyDefaults) { + // Test construction of an empty property table property. + draco::PropertyTable::Property property; + ASSERT_TRUE(property.GetName().empty()); + ASSERT_TRUE(property.GetData().data.empty()); + { + const auto &offsets = property.GetArrayOffsets(); + ASSERT_TRUE(offsets.type.empty()); + ASSERT_TRUE(offsets.data.data.empty()); + ASSERT_EQ(offsets.data.target, 0); + } + { + const auto &offsets = property.GetStringOffsets(); + ASSERT_TRUE(offsets.type.empty()); + ASSERT_TRUE(offsets.data.data.empty()); + ASSERT_EQ(offsets.data.target, 0); + } +} + +TEST(PropertyTableTest, TestPropertyTableDefaults) { + // Test construction of an empty property table. + draco::PropertyTable table; + ASSERT_TRUE(table.GetName().empty()); + ASSERT_TRUE(table.GetClass().empty()); + ASSERT_EQ(table.GetCount(), 0); + ASSERT_EQ(table.NumProperties(), 0); +} + +TEST(PropertyTableTest, TestSchemaDefaults) { + // Test construction of an empty property table schema. + draco::PropertyTable::Schema schema; + ASSERT_TRUE(schema.Empty()); + ASSERT_EQ(schema.json.GetName(), "schema"); + ASSERT_EQ(schema.json.GetType(), + draco::PropertyTable::Schema::Object::OBJECT); + ASSERT_TRUE(schema.json.GetObjects().empty()); + ASSERT_TRUE(schema.json.GetArray().empty()); + ASSERT_TRUE(schema.json.GetString().empty()); + ASSERT_EQ(schema.json.GetInteger(), 0); + ASSERT_FALSE(schema.json.GetBoolean()); +} + +TEST(PropertyTableTest, TestSchemaObjectDefaultConstructor) { + // Test construction of an empty property table schema object. + draco::PropertyTable::Schema::Object object; + ASSERT_TRUE(object.GetName().empty()); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::OBJECT); + ASSERT_TRUE(object.GetObjects().empty()); + ASSERT_TRUE(object.GetArray().empty()); + ASSERT_TRUE(object.GetString().empty()); + ASSERT_EQ(object.GetInteger(), 0); + ASSERT_FALSE(object.GetBoolean()); +} + +TEST(PropertyTableTest, TestSchemaObjectNamedConstructor) { + // Test construction of a named property table schema object. + draco::PropertyTable::Schema::Object object("Flexible Demeanour"); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::OBJECT); + ASSERT_TRUE(object.GetObjects().empty()); +} + +TEST(PropertyTableTest, TestSchemaObjectStringConstructor) { + // Test construction of property table schema object storing a string. + draco::PropertyTable::Schema::Object object("Flexible Demeanour", "GCU"); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::STRING); + ASSERT_EQ(object.GetString(), "GCU"); +} + +TEST(PropertyTableTest, TestSchemaObjectIntegerConstructor) { + // Test construction of property table schema object storing an integer. + draco::PropertyTable::Schema::Object object("Flexible Demeanour", 12); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::INTEGER); + ASSERT_EQ(object.GetInteger(), 12); +} + +TEST(PropertyTableTest, TestSchemaObjectBooleanConstructor) { + // Test construction of property table schema object storing a boolean. + draco::PropertyTable::Schema::Object object("Flexible Demeanour", true); + ASSERT_EQ(object.GetName(), "Flexible Demeanour"); + ASSERT_EQ(object.GetType(), draco::PropertyTable::Schema::Object::BOOLEAN); + ASSERT_TRUE(object.GetBoolean()); +} + +TEST(PropertyTableTest, TestSchemaObjectSettersAndGetters) { + // Test value setters and getters of property table schema object. + typedef draco::PropertyTable::Schema::Object Object; + Object object; + ASSERT_EQ(object.GetType(), Object::OBJECT); + + object.SetArray().push_back(Object("entry", 12)); + ASSERT_EQ(object.GetType(), Object::ARRAY); + ASSERT_EQ(object.GetArray().size(), 1); + ASSERT_EQ(object.GetArray()[0].GetName(), "entry"); + ASSERT_EQ(object.GetArray()[0].GetInteger(), 12); + + object.SetObjects().push_back(Object("object", 9)); + ASSERT_EQ(object.GetType(), Object::OBJECT); + ASSERT_EQ(object.GetObjects().size(), 1); + ASSERT_EQ(object.GetObjects()[0].GetName(), "object"); + ASSERT_EQ(object.GetObjects()[0].GetInteger(), 9); + + object.SetString("matter"); + ASSERT_EQ(object.GetType(), Object::STRING); + ASSERT_EQ(object.GetString(), "matter"); + + object.SetInteger(5); + ASSERT_EQ(object.GetType(), Object::INTEGER); + ASSERT_EQ(object.GetInteger(), 5); + + object.SetBoolean(true); + ASSERT_EQ(object.GetType(), Object::BOOLEAN); + ASSERT_EQ(object.GetBoolean(), true); +} + +TEST(PropertyTableTest, TestSchemaCompare) { + typedef draco::PropertyTable::Schema Schema; + // Test comparison of two schema objects. + { + // Compare the same empty schema object. + Schema a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two empty schema objects. + Schema a; + Schema b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two schema objects with different JSON objects. + Schema a; + Schema b; + a.json.SetBoolean(true); + b.json.SetBoolean(false); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestSchemaObjectCompare) { + // Test comparison of two schema JSON objects. + typedef draco::PropertyTable::Schema::Object Object; + { + // Compare the same object. + Object a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default objects. + Object a; + Object b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two objects with different names. + Object a("one"); + Object b("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two objects with different types. + Object a; + Object b; + a.SetInteger(1); + b.SetString("one"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical string-type objects. + Object a; + Object b; + a.SetString("one"); + b.SetString("one"); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different string-type objects. + Object a; + Object b; + a.SetString("one"); + b.SetString("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical integer-type objects. + Object a; + Object b; + a.SetInteger(1); + b.SetInteger(1); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different integer-type objects. + Object a; + Object b; + a.SetInteger(1); + b.SetInteger(2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical boolean-type objects. + Object a; + Object b; + a.SetBoolean(true); + b.SetBoolean(true); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different boolean-type objects. + Object a; + Object b; + a.SetBoolean(true); + b.SetBoolean(false); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical object-type objects. + Object a; + Object b; + a.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("one"); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different object-type objects. + Object a; + Object b; + a.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two object-type objects with different counts. + Object a; + Object b; + a.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("one"); + b.SetObjects().emplace_back("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two identical array-type objects. + Object a; + Object b; + a.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 1); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two different array-type objects. + Object a; + Object b; + a.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two array-type objects with different counts. + Object a; + Object b; + a.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 1); + b.SetArray().emplace_back("", 2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertySettersAndGetters) { + // Test setter and getter methods of the property table property. + draco::PropertyTable::Property property; + property.SetName("Unfortunate Conflict Of Evidence"); + property.GetData().data.push_back(2); + + // Check that property members can be accessed via getters. + ASSERT_EQ(property.GetName(), "Unfortunate Conflict Of Evidence"); + ASSERT_EQ(property.GetData().data.size(), 1); + ASSERT_EQ(property.GetData().data[0], 2); +} + +TEST(PropertyTableTest, TestPropertyTableSettersAndGetters) { + // Test setter and getter methods of the property table. + draco::PropertyTable table; + table.SetName("Just Read The Instructions"); + table.SetClass("General Contact Unit"); + table.SetCount(456); + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Determinist"); + ASSERT_EQ(table.AddProperty(std::move(property)), 0); + } + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Revisionist"); + ASSERT_EQ(table.AddProperty(std::move(property)), 1); + } + + // Check that property table members can be accessed via getters. + ASSERT_EQ(table.GetName(), "Just Read The Instructions"); + ASSERT_EQ(table.GetClass(), "General Contact Unit"); + ASSERT_EQ(table.GetCount(), 456); + ASSERT_EQ(table.NumProperties(), 2); + ASSERT_EQ(table.GetProperty(0).GetName(), "Determinist"); + ASSERT_EQ(table.GetProperty(1).GetName(), "Revisionist"); + + // Check that proeprties can be removed. + table.RemoveProperty(0); + ASSERT_EQ(table.NumProperties(), 1); + ASSERT_EQ(table.GetProperty(0).GetName(), "Revisionist"); + table.RemoveProperty(0); + ASSERT_EQ(table.NumProperties(), 0); +} + +TEST(PropertyTableTest, TestPropertyCopy) { + // Test that property table property can be copied. + draco::PropertyTable::Property property; + property.SetName("Unfortunate Conflict Of Evidence"); + property.GetData().data.push_back(2); + + // Make a copy. + draco::PropertyTable::Property copy; + copy.Copy(property); + + // Check the copy. + ASSERT_EQ(copy.GetName(), "Unfortunate Conflict Of Evidence"); + ASSERT_EQ(copy.GetData().data.size(), 1); + ASSERT_EQ(copy.GetData().data[0], 2); +} + +TEST(PropertyTableTest, TestPropertyTableCopy) { + // Test that property table can be copied. + draco::PropertyTable table; + table.SetName("Just Read The Instructions"); + table.SetClass("General Contact Unit"); + table.SetCount(456); + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Determinist"); + table.AddProperty(std::move(property)); + } + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Revisionist"); + table.AddProperty(std::move(property)); + } + + // Make a copy. + draco::PropertyTable copy; + copy.Copy(table); + + // Check the copy. + ASSERT_EQ(copy.GetName(), "Just Read The Instructions"); + ASSERT_EQ(copy.GetClass(), "General Contact Unit"); + ASSERT_EQ(copy.GetCount(), 456); + ASSERT_EQ(copy.NumProperties(), 2); + ASSERT_EQ(copy.GetProperty(0).GetName(), "Determinist"); + ASSERT_EQ(copy.GetProperty(1).GetName(), "Revisionist"); +} + +TEST(PropertyTableTest, TestPropertyDataCompare) { + // Test comparison of two property data objects. + typedef draco::PropertyTable::Property::Data Data; + { + // Compare the same data object. + Data a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default data objects. + Data a; + Data b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two data objects with different targets. + Data a; + Data b; + a.target = 1; + b.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two data objects with different data vectors. + Data a; + Data b; + a.data = {1}; + a.data = {2}; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertyOffsets) { + // Test comparison of two property offsets. + typedef draco::PropertyTable::Property::Offsets Offsets; + { + // Compare the same offsets object. + Offsets a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default offsets objects. + Offsets a; + Offsets b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two offsets objects with different types. + Offsets a; + Offsets b; + a.type = 1; + b.type = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two offsets objects with different data objects. + Offsets a; + Offsets b; + a.data.target = 1; + b.data.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertyCompare) { + // Test comparison of two properties. + typedef draco::PropertyTable::Property Property; + { + // Compare the same property object. + Property a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default property objects. + Property a; + Property b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two property objects with different names. + Property a; + Property b; + a.SetName("one"); + b.SetName("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property objects with different data. + Property a; + Property b; + a.GetData().target = 1; + b.GetData().target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property objects with different array offsets. + Property a; + Property b; + a.GetArrayOffsets().data.target = 1; + b.GetArrayOffsets().data.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property objects with different string offsets. + Property a; + Property b; + a.GetStringOffsets().data.target = 1; + b.GetStringOffsets().data.target = 2; + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +TEST(PropertyTableTest, TestPropertyTableCompare) { + // Test comparison of two property tables. + typedef draco::PropertyTable PropertyTable; + typedef draco::PropertyTable::Property Property; + { + // Compare the same property table object. + PropertyTable a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two default property tables. + PropertyTable a; + PropertyTable b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two property tables with different names. + PropertyTable a; + PropertyTable b; + a.SetName("one"); + b.SetName("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with different classes. + PropertyTable a; + PropertyTable b; + a.SetClass("one"); + b.SetClass("two"); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with different counts. + PropertyTable a; + PropertyTable b; + a.SetCount(1); + b.SetCount(2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with identical properties. + PropertyTable a; + PropertyTable b; + a.AddProperty(std::unique_ptr(new Property)); + b.AddProperty(std::unique_ptr(new Property)); + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two property tables with different number of properties. + PropertyTable a; + PropertyTable b; + a.AddProperty(std::unique_ptr(new Property)); + b.AddProperty(std::unique_ptr(new Property)); + b.AddProperty(std::unique_ptr(new Property)); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two property tables with different properties. + PropertyTable a; + PropertyTable b; + std::unique_ptr p1(new Property); + std::unique_ptr p2(new Property); + p1->SetName("one"); + p2->SetName("two"); + a.AddProperty(std::move(p1)); + b.AddProperty(std::move(p2)); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/metadata/structural_metadata.cc b/contrib/draco/src/draco/metadata/structural_metadata.cc new file mode 100644 index 000000000..48fff2b25 --- /dev/null +++ b/contrib/draco/src/draco/metadata/structural_metadata.cc @@ -0,0 +1,74 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/metadata/structural_metadata.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +StructuralMetadata::StructuralMetadata() {} + +bool StructuralMetadata::operator==(const StructuralMetadata &other) const { + return property_table_schema_ == other.property_table_schema_ && + property_tables_ == other.property_tables_; +} + +void StructuralMetadata::Copy(const StructuralMetadata &src) { + property_table_schema_.json.Copy(src.property_table_schema_.json); + property_tables_.resize(src.property_tables_.size()); + for (int i = 0; i < property_tables_.size(); ++i) { + property_tables_[i] = std::unique_ptr(new PropertyTable()); + property_tables_[i]->Copy(*src.property_tables_[i]); + } +} + +void StructuralMetadata::SetPropertyTableSchema( + const PropertyTable::Schema &schema) { + property_table_schema_ = schema; +} + +const PropertyTable::Schema &StructuralMetadata::GetPropertyTableSchema() + const { + return property_table_schema_; +} + +int StructuralMetadata::AddPropertyTable( + std::unique_ptr property_table) { + property_tables_.push_back(std::move(property_table)); + return property_tables_.size() - 1; +} + +int StructuralMetadata::NumPropertyTables() const { + return property_tables_.size(); +} + +const PropertyTable &StructuralMetadata::GetPropertyTable(int index) const { + return *property_tables_[index]; +} + +PropertyTable &StructuralMetadata::GetPropertyTable(int index) { + return *property_tables_[index]; +} + +void StructuralMetadata::RemovePropertyTable(int index) { + property_tables_.erase(property_tables_.begin() + index); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/metadata/structural_metadata.h b/contrib/draco/src/draco/metadata/structural_metadata.h new file mode 100644 index 000000000..24756c70c --- /dev/null +++ b/contrib/draco/src/draco/metadata/structural_metadata.h @@ -0,0 +1,64 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_METADATA_STRUCTURAL_METADATA_H_ +#define DRACO_METADATA_STRUCTURAL_METADATA_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include +#include +#include + +#include "draco/metadata/property_table.h" + +namespace draco { + +// Holds data associated with EXT_structural_metadata glTF extension. +class StructuralMetadata { + public: + StructuralMetadata(); + + // Methods for comparing two structural metadata objects. + bool operator==(const StructuralMetadata &other) const; + bool operator!=(const StructuralMetadata &other) const { + return !(*this == other); + } + + // Copies |src| structural metadata into this object. + void Copy(const StructuralMetadata &src); + + // Property table schema. + void SetPropertyTableSchema(const PropertyTable::Schema &schema); + const PropertyTable::Schema &GetPropertyTableSchema() const; + + // Property tables. + int AddPropertyTable(std::unique_ptr property_table); + int NumPropertyTables() const; + const PropertyTable &GetPropertyTable(int index) const; + PropertyTable &GetPropertyTable(int index); + void RemovePropertyTable(int index); + + private: + // Property table schema and property tables. + PropertyTable::Schema property_table_schema_; + std::vector> property_tables_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_METADATA_STRUCTURAL_METADATA_H_ diff --git a/contrib/draco/src/draco/metadata/structural_metadata_test.cc b/contrib/draco/src/draco/metadata/structural_metadata_test.cc new file mode 100644 index 000000000..d0429a568 --- /dev/null +++ b/contrib/draco/src/draco/metadata/structural_metadata_test.cc @@ -0,0 +1,170 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/metadata/structural_metadata.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(StructuralMetadataTest, TestCopy) { + // Tests copying of structural metadata. + draco::StructuralMetadata structural_metadata; + + // Add property table schema to structural metadata. + draco::PropertyTable::Schema schema; + schema.json.SetString("Culture"); + structural_metadata.SetPropertyTableSchema(schema); + + // Add property table to structural metadata. + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("Just Read The Instructions"); + table->SetClass("General Contact Unit"); + table->SetCount(456); + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Determinist"); + table->AddProperty(std::move(property)); + } + { + std::unique_ptr property( + new draco::PropertyTable::Property()); + property->SetName("Revisionist"); + table->AddProperty(std::move(property)); + } + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 0); + + // Copy the structural metadata. + draco::StructuralMetadata copy; + copy.Copy(structural_metadata); + + // Check that the structural metadata property table schema has been copied. + ASSERT_EQ(copy.GetPropertyTableSchema().json.GetString(), "Culture"); + + // Check that the structural metadata property table has been copied. + ASSERT_EQ(copy.NumPropertyTables(), 1); + ASSERT_EQ(copy.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + ASSERT_EQ(copy.GetPropertyTable(0).GetClass(), "General Contact Unit"); + ASSERT_EQ(copy.GetPropertyTable(0).GetCount(), 456); + ASSERT_EQ(copy.GetPropertyTable(0).NumProperties(), 2); + ASSERT_EQ(copy.GetPropertyTable(0).GetProperty(0).GetName(), "Determinist"); + ASSERT_EQ(copy.GetPropertyTable(0).GetProperty(1).GetName(), "Revisionist"); +} + +TEST(StructuralMetadataTest, TestPropertyTables) { + // Tests adding and removing of property tables to structural metadata. + draco::StructuralMetadata structural_metadata; + + // Check that property tables can be added. + { + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("Just Read The Instructions"); + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 0); + } + { + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("So Much For Subtlety"); + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 1); + } + { + std::unique_ptr table(new draco::PropertyTable()); + table->SetName("Of Course I Still Love You"); + ASSERT_EQ(structural_metadata.AddPropertyTable(std::move(table)), 2); + } + draco::StructuralMetadata &sm = structural_metadata; + + // Check that the property tables can be removed. + ASSERT_EQ(sm.NumPropertyTables(), 3); + ASSERT_EQ(sm.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + ASSERT_EQ(sm.GetPropertyTable(1).GetName(), "So Much For Subtlety"); + ASSERT_EQ(sm.GetPropertyTable(2).GetName(), "Of Course I Still Love You"); + + sm.RemovePropertyTable(1); + ASSERT_EQ(sm.NumPropertyTables(), 2); + ASSERT_EQ(sm.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + ASSERT_EQ(sm.GetPropertyTable(1).GetName(), "Of Course I Still Love You"); + + sm.RemovePropertyTable(1); + ASSERT_EQ(sm.NumPropertyTables(), 1); + ASSERT_EQ(sm.GetPropertyTable(0).GetName(), "Just Read The Instructions"); + + sm.RemovePropertyTable(0); + ASSERT_EQ(sm.NumPropertyTables(), 0); +} + +TEST(StructuralMetadataTest, TestCompare) { + // Test comparison of two structural metadata objects. + typedef draco::PropertyTable PropertyTable; + { + // Compare the same structural metadata object. + draco::StructuralMetadata a; + ASSERT_TRUE(a == a); + ASSERT_FALSE(a != a); + } + { + // Compare two identical structural metadata objects. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + ASSERT_TRUE(a == b); + ASSERT_FALSE(a != b); + } + { + // Compare two structural metadata objects with different schemas. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + PropertyTable::Schema s1; + PropertyTable::Schema s2; + s1.json.SetString("one"); + s2.json.SetString("two"); + a.SetPropertyTableSchema(s1); + b.SetPropertyTableSchema(s2); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two objects with different number of proeprty tables. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + a.AddPropertyTable(std::unique_ptr(new PropertyTable())); + b.AddPropertyTable(std::unique_ptr(new PropertyTable())); + b.AddPropertyTable(std::unique_ptr(new PropertyTable())); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } + { + // Compare two objects with different proeprty tables. + draco::StructuralMetadata a; + draco::StructuralMetadata b; + auto p1 = std::unique_ptr(new PropertyTable()); + auto p2 = std::unique_ptr(new PropertyTable()); + p1->SetName("one"); + p2->SetName("two"); + a.AddPropertyTable(std::move(p1)); + b.AddPropertyTable(std::move(p2)); + ASSERT_FALSE(a == b); + ASSERT_TRUE(a != b); + } +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/point_cloud/point_cloud.cc b/contrib/draco/src/draco/point_cloud/point_cloud.cc index a9f9ea2af..039c4f201 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud.cc +++ b/contrib/draco/src/draco/point_cloud/point_cloud.cc @@ -16,11 +16,41 @@ #include #include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/attributes/point_attribute.h" +#endif namespace draco { PointCloud::PointCloud() : num_points_(0) {} +#ifdef DRACO_TRANSCODER_SUPPORTED +void PointCloud::Copy(const PointCloud &src) { + num_points_ = src.num_points_; + for (int i = 0; i < GeometryAttribute::NAMED_ATTRIBUTES_COUNT; ++i) { + named_attribute_index_[i] = src.named_attribute_index_[i]; + } + attributes_.resize(src.attributes_.size()); + for (int i = 0; i < src.attributes_.size(); ++i) { + attributes_[i] = std::unique_ptr(new PointAttribute()); + attributes_[i]->CopyFrom(*src.attributes_[i]); + } + CopyMetadata(src); +} + +void PointCloud::CopyMetadata(const PointCloud &src) { + if (src.metadata_ == nullptr) { + metadata_ = nullptr; + } else { + // Copy base metadata. + const GeometryMetadata *const metadata = src.metadata_.get(); + metadata_.reset(new GeometryMetadata(*metadata)); + } +} +#endif + int32_t PointCloud::NumNamedAttributes(GeometryAttribute::Type type) const { if (type == GeometryAttribute::INVALID || type >= GeometryAttribute::NAMED_ATTRIBUTES_COUNT) { @@ -253,11 +283,16 @@ bool PointCloud::DeduplicateAttributeValues() { } #endif -// TODO(xiaoxumeng): Consider to cash the BBox. +// TODO(b/199760503): Consider to cache the BBox. BoundingBox PointCloud::ComputeBoundingBox() const { BoundingBox bounding_box; auto pc_att = GetNamedAttribute(GeometryAttribute::POSITION); - // TODO(xiaoxumeng): Make the BoundingBox a template type, it may not be easy + if (pc_att == nullptr) { + // Return default invalid bounding box. + return bounding_box; + } + + // TODO(b/199760503): Make the BoundingBox a template type, it may not be easy // because PointCloud is not a template. // Or simply add some preconditioning here to make sure the position attribute // is valid, because the current code works only if the position attribute is diff --git a/contrib/draco/src/draco/point_cloud/point_cloud.h b/contrib/draco/src/draco/point_cloud/point_cloud.h index d11bd47a3..17d0bc392 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud.h +++ b/contrib/draco/src/draco/point_cloud/point_cloud.h @@ -31,6 +31,11 @@ class PointCloud { PointCloud(); virtual ~PointCloud() = default; +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copies all data from the |src| point cloud. + void Copy(const PointCloud &src); +#endif + // Returns the number of named attributes of a given type. int32_t NumNamedAttributes(GeometryAttribute::Type type) const; @@ -185,6 +190,11 @@ class PointCloud { void set_num_points(PointIndex::ValueType num) { num_points_ = num; } protected: +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copies metadata from the |src| point cloud. + void CopyMetadata(const PointCloud &src); +#endif + #ifdef DRACO_ATTRIBUTE_INDICES_DEDUPLICATION_SUPPORTED // Applies id mapping of deduplicated points (called by DeduplicatePointIds). virtual void ApplyPointIdDeduplication( diff --git a/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc b/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc index 431ae505f..90ec37962 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc +++ b/contrib/draco/src/draco/point_cloud/point_cloud_builder.cc @@ -14,6 +14,8 @@ // #include "draco/point_cloud/point_cloud_builder.h" +#include + namespace draco { PointCloudBuilder::PointCloudBuilder() {} diff --git a/contrib/draco/src/draco/point_cloud/point_cloud_builder.h b/contrib/draco/src/draco/point_cloud/point_cloud_builder.h index cf55a728b..512b0f71f 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud_builder.h +++ b/contrib/draco/src/draco/point_cloud/point_cloud_builder.h @@ -15,6 +15,8 @@ #ifndef DRACO_POINT_CLOUD_POINT_CLOUD_BUILDER_H_ #define DRACO_POINT_CLOUD_POINT_CLOUD_BUILDER_H_ +#include + #include "draco/point_cloud/point_cloud.h" namespace draco { @@ -37,6 +39,9 @@ namespace draco { class PointCloudBuilder { public: + // Index type of the inserted element. + typedef PointIndex ElementIndex; + PointCloudBuilder(); // Starts collecting point cloud data. @@ -71,6 +76,12 @@ class PointCloudBuilder { // used until the method Start() is called again. std::unique_ptr Finalize(bool deduplicate_points); + // Add metadata for an attribute. + void AddAttributeMetadata(int32_t att_id, + std::unique_ptr metadata) { + point_cloud_->AddAttributeMetadata(att_id, std::move(metadata)); + } + private: std::unique_ptr point_cloud_; }; diff --git a/contrib/draco/src/draco/point_cloud/point_cloud_test.cc b/contrib/draco/src/draco/point_cloud/point_cloud_test.cc index 4e9460370..1cd780db2 100644 --- a/contrib/draco/src/draco/point_cloud/point_cloud_test.cc +++ b/contrib/draco/src/draco/point_cloud/point_cloud_test.cc @@ -14,6 +14,9 @@ // #include "draco/point_cloud/point_cloud.h" +#include +#include + #include "draco/core/draco_test_base.h" #include "draco/core/draco_test_utils.h" #include "draco/metadata/geometry_metadata.h" @@ -25,6 +28,57 @@ class PointCloudTest : public ::testing::Test { PointCloudTest() {} }; +#ifdef DRACO_TRANSCODER_SUPPORTED +TEST_F(PointCloudTest, PointCloudCopy) { + // Tests that we can copy a point cloud. + std::unique_ptr pc = + draco::ReadPointCloudFromTestFile("pc_kd_color.drc"); + ASSERT_NE(pc, nullptr); + + // Add metadata to the point cloud. + std::unique_ptr metadata( + new draco::GeometryMetadata()); + metadata->AddEntryInt("speed", 1050); + metadata->AddEntryString("code", "YT-1300f"); + + // Add attribute metadata. + std::unique_ptr a_metadata( + new draco::AttributeMetadata()); + a_metadata->set_att_unique_id(pc->attribute(0)->unique_id()); + a_metadata->AddEntryInt("attribute_test", 3); + metadata->AddAttributeMetadata(std::move(a_metadata)); + pc->AddMetadata(std::move(metadata)); + + // Create a copy of the point cloud. + draco::PointCloud pc_copy; + pc_copy.Copy(*pc); + + // Check the point cloud data. + ASSERT_EQ(pc->num_points(), pc_copy.num_points()); + ASSERT_EQ(pc->num_attributes(), pc_copy.num_attributes()); + for (int i = 0; i < pc->num_attributes(); ++i) { + ASSERT_EQ(pc->attribute(i)->attribute_type(), + pc_copy.attribute(i)->attribute_type()); + } + + // Check the point cloud metadata. + int speed; + std::string code; + ASSERT_NE(pc->GetMetadata(), nullptr); + ASSERT_TRUE(pc->GetMetadata()->GetEntryInt("speed", &speed)); + ASSERT_TRUE(pc->GetMetadata()->GetEntryString("code", &code)); + ASSERT_EQ(speed, 1050); + ASSERT_EQ(code, "YT-1300f"); + + const auto *const att_metadata_copy = + pc->GetMetadata()->GetAttributeMetadataByUniqueId(0); + ASSERT_NE(att_metadata_copy, nullptr); + int att_test; + ASSERT_TRUE(att_metadata_copy->GetEntryInt("attribute_test", &att_test)); + ASSERT_EQ(att_test, 3); +} +#endif + TEST_F(PointCloudTest, TestAttributeDeletion) { draco::PointCloud pc; // Test whether we can correctly delete an attribute from a point cloud. diff --git a/contrib/draco/src/draco/scene/instance_array.cc b/contrib/draco/src/draco/scene/instance_array.cc new file mode 100644 index 000000000..bcd49996a --- /dev/null +++ b/contrib/draco/src/draco/scene/instance_array.cc @@ -0,0 +1,45 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/instance_array.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include + +namespace draco { + +void InstanceArray::Copy(const InstanceArray &other) { + instances_.resize(other.instances_.size()); + for (int i = 0; i < instances_.size(); i++) { + instances_[i].trs.Copy(other.instances_[i].trs); + } +} + +Status InstanceArray::AddInstance(const Instance &instance) { + // Check that the |instance.trs| does not have the transformation matrix set, + // because the EXT_mesh_gpu_instancing glTF extension dictates that only the + // individual TRS vectors are stored. + if (instance.trs.MatrixSet()) { + return ErrorStatus("Instance must have no matrix set."); + } + + // Move the |instance| to the end of the instances vector. + instances_.push_back(instance); + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/instance_array.h b/contrib/draco/src/draco/scene/instance_array.h new file mode 100644 index 000000000..fb1fbde1e --- /dev/null +++ b/contrib/draco/src/draco/scene/instance_array.h @@ -0,0 +1,61 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_SCENE_INSTANCE_ARRAY_H_ +#define DRACO_SCENE_INSTANCE_ARRAY_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/vector_d.h" +#include "draco/scene/trs_matrix.h" + +namespace draco { + +// Describes a mesh group instancing array that includes TRS transformation +// for multiple instance positions and possibly other custom instance attributes +// (not yet supported), following the EXT_mesh_gpu_instancing glTF extension. +class InstanceArray { + public: + struct Instance { + // Translation, rotation, and scale vectors. + TrsMatrix trs; + // TODO(vytyaz): Support custom instance attributes, e.g., _ID, _COLOR, etc. + }; + + InstanceArray() = default; + + void Copy(const InstanceArray &other); + + // Adds one |instance| into this mesh group instance array where the + // |instance.trs| may have optional translation, rotation, and scale set. + Status AddInstance(const Instance &instance); + + // Returns the count of instances in this mesh group instance array. + int NumInstances() const { return instances_.size(); } + + // Returns an instance from this mesh group instance array. + const Instance &GetInstance(int index) const { return instances_[index]; } + + private: + std::vector instances_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_INSTANCE_ARRAY_H_ diff --git a/contrib/draco/src/draco/scene/instance_array_test.cc b/contrib/draco/src/draco/scene/instance_array_test.cc new file mode 100644 index 000000000..d3802f901 --- /dev/null +++ b/contrib/draco/src/draco/scene/instance_array_test.cc @@ -0,0 +1,179 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/instance_array.h" + +#include +#include + +#include "draco/core/constants.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(InstanceArrayTest, TestInstance) { + // Test construction of an empty draco::InstanceArray::Instance struct. + const draco::InstanceArray::Instance instance; + ASSERT_FALSE(instance.trs.TranslationSet()); + ASSERT_FALSE(instance.trs.RotationSet()); + ASSERT_FALSE(instance.trs.ScaleSet()); + ASSERT_FALSE(instance.trs.MatrixSet()); +} + +TEST(InstanceArrayTest, TestDefaults) { + // Test construction of an empty draco::InstanceArray object. + const draco::InstanceArray array; + ASSERT_EQ(array.NumInstances(), 0); +} + +TEST(InstanceArrayTest, TestAddInstance) { + // Test population of draco::InstanceArray object with instances. + draco::InstanceArray array; + + // Create an instance and set its transformation TRS vectors. + const Eigen::Vector3d translation_0(1.0, 2.0, 3.0); + const Eigen::Quaterniond rotation_0(4.0, 5.0, 6.0, 7.0); + const Eigen::Vector3d scale_0(8.0, 9.0, 10.0); + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(translation_0); + instance_0.trs.SetRotation(rotation_0); + instance_0.trs.SetScale(scale_0); + + // Create another instance. + const Eigen::Vector3d translation_1(1.1, 2.1, 3.1); + const Eigen::Quaterniond rotation_1(4.1, 5.1, 6.1, 7.1); + const Eigen::Vector3d scale_1(8.1, 9.1, 10.1); + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(translation_1); + instance_1.trs.SetRotation(rotation_1); + instance_1.trs.SetScale(scale_1); + + // Add two instances to instance array. + DRACO_ASSERT_OK(array.AddInstance(instance_0)); + DRACO_ASSERT_OK(array.AddInstance(instance_1)); + + // Check that the instances have been added. + ASSERT_EQ(array.NumInstances(), 2); + + // Check transformation of the first instance. + const draco::TrsMatrix &trs_0 = array.GetInstance(0).trs; + ASSERT_TRUE(trs_0.TranslationSet()); + ASSERT_TRUE(trs_0.RotationSet()); + ASSERT_TRUE(trs_0.ScaleSet()); + ASSERT_FALSE(trs_0.MatrixSet()); + ASSERT_EQ(trs_0.Translation().value(), translation_0); + ASSERT_EQ(trs_0.Rotation().value(), rotation_0); + ASSERT_EQ(trs_0.Scale().value(), scale_0); + + // Check transformation of the second instance. + const draco::TrsMatrix &trs_1 = array.GetInstance(1).trs; + ASSERT_TRUE(trs_1.TranslationSet()); + ASSERT_TRUE(trs_1.RotationSet()); + ASSERT_TRUE(trs_1.ScaleSet()); + ASSERT_FALSE(trs_1.MatrixSet()); + ASSERT_EQ(trs_1.Translation().value(), translation_1); + ASSERT_EQ(trs_1.Rotation().value(), rotation_1); + ASSERT_EQ(trs_1.Scale().value(), scale_1); +} + +TEST(InstanceArrayTest, TestAddInstanceWithoutTransform) { + // Test that instance without any transformation can be added. + draco::InstanceArray array; + + // Do not set any transformation. + draco::InstanceArray::Instance instance; + + // Check that such instance can be added. + DRACO_ASSERT_OK(array.AddInstance(instance)); +} + +TEST(InstanceArrayTest, TestAddInstanceWithoutScale) { + // Test that instance without scale can be added. + draco::InstanceArray array; + + // Set only instance translation and rotation. + draco::InstanceArray::Instance instance; + instance.trs.SetTranslation(Eigen::Vector3d(1.0, 2.0, 3.0)); + instance.trs.SetRotation(Eigen::Quaterniond(4.0, 5.0, 6.0, 7.0)); + + // Check that such instance can be added. + DRACO_ASSERT_OK(array.AddInstance(instance)); +} + +TEST(InstanceArrayTest, TestAddInstanceWithMatrixFails) { + // Test that instance without scale cannot be added. + draco::InstanceArray array; + + // Set TRS vectors, as well as the matrix. + draco::InstanceArray::Instance instance; + instance.trs.SetTranslation(Eigen::Vector3d(1.0, 2.0, 3.0)); + instance.trs.SetRotation(Eigen::Quaterniond(4.0, 5.0, 6.0, 7.0)); + instance.trs.SetScale(Eigen::Vector3d(8.0, 9.0, 10.0)); + // clang-format off + Eigen::Matrix4d matrix; + matrix << 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0; + // clang-format on + instance.trs.SetMatrix(matrix); + + // Check that such instance cannot be added. + const draco::Status status = array.AddInstance(instance); + ASSERT_FALSE(status.ok()); + ASSERT_EQ(status.error_msg_string(), "Instance must have no matrix set."); +} + +TEST(InstanceArrayTest, TestCopy) { + // Test copying of draco::InstanceArray object. + draco::InstanceArray array; + + // Create an instance and set its transformation TRS vectors. + const Eigen::Vector3d translation_0(1.0, 2.0, 3.0); + const Eigen::Quaterniond rotation_0(4.0, 5.0, 6.0, 7.0); + const Eigen::Vector3d scale_0(8.0, 9.0, 10.0); + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(translation_0); + instance_0.trs.SetRotation(rotation_0); + instance_0.trs.SetScale(scale_0); + + // Create another instance. + const Eigen::Vector3d translation_1(1.1, 2.1, 3.1); + const Eigen::Quaterniond rotation_1(4.1, 5.1, 6.1, 7.1); + const Eigen::Vector3d scale_1(8.1, 9.1, 10.1); + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(translation_1); + instance_1.trs.SetRotation(rotation_1); + instance_1.trs.SetScale(scale_1); + + // Add two instances to the instance array. + DRACO_ASSERT_OK(array.AddInstance(instance_0)); + DRACO_ASSERT_OK(array.AddInstance(instance_1)); + + // Create a copy of the populated instance array object. + draco::InstanceArray copy; + copy.Copy(array); + + // Check that the instances have been copied. + ASSERT_EQ(copy.NumInstances(), 2); + ASSERT_EQ(copy.GetInstance(0).trs, instance_0.trs); + ASSERT_EQ(copy.GetInstance(1).trs, instance_1.trs); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/light.cc b/contrib/draco/src/draco/scene/light.cc new file mode 100644 index 000000000..1fef98c0c --- /dev/null +++ b/contrib/draco/src/draco/scene/light.cc @@ -0,0 +1,45 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/light.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include + +#include "draco/core/constants.h" + +namespace draco { + +Light::Light() + : color_(1.0f, 1.0f, 1.0f), + intensity_(1.0), + type_(POINT), + range_(std::numeric_limits::max()), // Infinity. + inner_cone_angle_(0.0), + outer_cone_angle_(DRACO_PI / 4.0) {} + +void Light::Copy(const Light &light) { + name_ = light.name_; + color_ = light.color_; + intensity_ = light.intensity_; + type_ = light.type_; + range_ = light.range_; + inner_cone_angle_ = light.inner_cone_angle_; + outer_cone_angle_ = light.outer_cone_angle_; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/light.h b/contrib/draco/src/draco/scene/light.h new file mode 100644 index 000000000..5ff0d4a6b --- /dev/null +++ b/contrib/draco/src/draco/scene/light.h @@ -0,0 +1,81 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_SCENE_LIGHT_H_ +#define DRACO_SCENE_LIGHT_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/core/vector_d.h" + +namespace draco { + +// Describes a light in a scene according to the KHR_lights_punctual extension. +class Light { + public: + enum Type { DIRECTIONAL, POINT, SPOT }; + + Light(); + + void Copy(const Light &light); + + // Name. + void SetName(const std::string &name) { name_ = name; } + const std::string &GetName() const { return name_; } + + // Color. + void SetColor(const Vector3f &color) { color_ = color; } + const Vector3f &GetColor() const { return color_; } + + // Intensity. + void SetIntensity(double intensity) { intensity_ = intensity; } + double GetIntensity() const { return intensity_; } + + // Type. + void SetType(Type type) { type_ = type; } + Type GetType() const { return type_; } + + // Range. + void SetRange(double range) { range_ = range; } + double GetRange() const { return range_; } + + // Inner cone angle. + void SetInnerConeAngle(double angle) { inner_cone_angle_ = angle; } + double GetInnerConeAngle() const { return inner_cone_angle_; } + + // Outer cone angle. + void SetOuterConeAngle(double angle) { outer_cone_angle_ = angle; } + double GetOuterConeAngle() const { return outer_cone_angle_; } + + private: + std::string name_; + Vector3f color_; + double intensity_; + Type type_; + + // The range is only applicable to lights with Type::POINT or Type::SPOT. + double range_; + + // The cone angles are only applicable to lights with Type::SPOT. + double inner_cone_angle_; + double outer_cone_angle_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_LIGHT_H_ diff --git a/contrib/draco/src/draco/scene/light_test.cc b/contrib/draco/src/draco/scene/light_test.cc new file mode 100644 index 000000000..bc24a14ad --- /dev/null +++ b/contrib/draco/src/draco/scene/light_test.cc @@ -0,0 +1,64 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/light.h" + +#include + +#include "draco/core/constants.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(LightTest, TestDefaults) { + // Text constructing draco::Light object with default properties. + const draco::Light light; + ASSERT_EQ(light.GetName(), ""); + ASSERT_EQ(light.GetColor(), draco::Vector3f(1.0f, 1.0f, 1.0f)); + ASSERT_EQ(light.GetIntensity(), 1.0); + ASSERT_EQ(light.GetType(), draco::Light::POINT); + ASSERT_EQ(light.GetRange(), std::numeric_limits::max()); + ASSERT_EQ(light.GetInnerConeAngle(), 0.0); + ASSERT_EQ(light.GetOuterConeAngle(), DRACO_PI / 4.0); +} + +TEST(LightTest, TestCopy) { + // Test copying of draco::Light object. + draco::Light light; + light.SetName("The Star of Earendil"); + light.SetColor(draco::Vector3f(0.90, 0.97, 1.00)); + light.SetIntensity(5.0); + light.SetType(draco::Light::SPOT); + light.SetRange(1000.0); + light.SetInnerConeAngle(DRACO_PI / 8.0); + light.SetOuterConeAngle(DRACO_PI / 2.0); + + // Create a copy of the initialized light and check all properties. + draco::Light copy; + copy.Copy(light); + ASSERT_EQ(copy.GetName(), "The Star of Earendil"); + ASSERT_EQ(copy.GetColor(), draco::Vector3f(0.90, 0.97, 1.00)); + ASSERT_EQ(copy.GetIntensity(), 5.0); + ASSERT_EQ(copy.GetType(), draco::Light::SPOT); + ASSERT_EQ(copy.GetRange(), 1000.0); + ASSERT_EQ(copy.GetInnerConeAngle(), DRACO_PI / 8.0); + ASSERT_EQ(copy.GetOuterConeAngle(), DRACO_PI / 2.0); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/mesh_group.h b/contrib/draco/src/draco/scene/mesh_group.h new file mode 100644 index 000000000..852318f16 --- /dev/null +++ b/contrib/draco/src/draco/scene/mesh_group.h @@ -0,0 +1,138 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_SCENE_MESH_GROUP_H_ +#define DRACO_SCENE_MESH_GROUP_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/core/macros.h" +#include "draco/scene/scene_indices.h" + +namespace draco { + +// This class is used to hold ordered mesh instances that refer to one or more +// base meshes, materials, and materials variants mappings. +class MeshGroup { + public: + // Stores a mapping from material index to materials variant indices. Each + // mesh instance may have multiple such mappings associated with it. See glTF + // extension KHR_materials_variants for more details. + struct MaterialsVariantsMapping { + MaterialsVariantsMapping() = delete; + MaterialsVariantsMapping(int material, const std::vector &variants) + : material(material), variants(variants) {} + bool operator==(const MaterialsVariantsMapping &other) const { + if (material != other.material) { + return false; + } + if (variants != other.variants) { + return false; + } + return true; + } + bool operator!=(const MaterialsVariantsMapping &other) const { + return !(*this == other); + } + int material; + std::vector variants; + }; + + // Describes mesh instance stored in a mesh group, including base mesh index, + // material index, and materials variants mappings. + struct MeshInstance { + MeshInstance() = delete; + MeshInstance(MeshIndex mesh_index, int material_index) + : MeshInstance(mesh_index, material_index, {}) {} + MeshInstance(MeshIndex mesh_index, int material_index, + const std::vector + &materials_variants_mappings) + : mesh_index(mesh_index), + material_index(material_index), + materials_variants_mappings(materials_variants_mappings) {} + bool operator==(const MeshInstance &other) const { + if (mesh_index != other.mesh_index) { + return false; + } + if (material_index != other.material_index) { + return false; + } + if (materials_variants_mappings.size() != + other.materials_variants_mappings.size()) { + return false; + } + if (materials_variants_mappings != other.materials_variants_mappings) { + return false; + } + return true; + } + bool operator!=(const MeshInstance &other) const { + return !(*this == other); + } + MeshIndex mesh_index; + int material_index; + std::vector materials_variants_mappings; + }; + + MeshGroup() {} + + void Copy(const MeshGroup &mg) { + name_ = mg.name_; + mesh_instances_ = mg.mesh_instances_; + } + + const std::string &GetName() const { return name_; } + void SetName(const std::string &name) { name_ = name; } + + void AddMeshInstance(const MeshInstance &instance) { + mesh_instances_.push_back(instance); + } + + void SetMeshInstance(int index, const MeshInstance &instance) { + mesh_instances_[index] = instance; + } + + const MeshInstance &GetMeshInstance(int index) const { + return mesh_instances_[index]; + } + + MeshInstance &GetMeshInstance(int index) { return mesh_instances_[index]; } + + int NumMeshInstances() const { return mesh_instances_.size(); } + + // Removes all mesh instances referring to base mesh at |mesh_index|. + void RemoveMeshInstances(MeshIndex mesh_index) { + int i = 0; + while (i != mesh_instances_.size()) { + if (mesh_instances_[i].mesh_index == mesh_index) { + mesh_instances_.erase(mesh_instances_.begin() + i); + } else { + i++; + } + } + } + + private: + std::string name_; + std::vector mesh_instances_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_MESH_GROUP_H_ diff --git a/contrib/draco/src/draco/scene/mesh_group_test.cc b/contrib/draco/src/draco/scene/mesh_group_test.cc new file mode 100644 index 000000000..76f1bf33e --- /dev/null +++ b/contrib/draco/src/draco/scene/mesh_group_test.cc @@ -0,0 +1,196 @@ +// Copyright 2020 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/mesh_group.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/scene/scene_indices.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +using draco::MeshGroup; +using draco::MeshIndex; + +// Test helper method generates materials variants mappings based on a |seed|. +std::vector MakeMappings(int seed) { + MeshGroup::MaterialsVariantsMapping a(10 * seed + 0, {seed + 0, seed + 1}); + MeshGroup::MaterialsVariantsMapping b(10 * seed + 1, {seed + 2, seed + 3}); + return {a, b}; +} + +TEST(MeshGroupTest, TestMeshInstanceTwoArgumentConstructor) { + MeshGroup::MeshInstance instance(MeshIndex(2), 3); + ASSERT_EQ(instance.mesh_index, MeshIndex(2)); + ASSERT_EQ(instance.material_index, 3); + ASSERT_EQ(instance.materials_variants_mappings.size(), 0); +} + +TEST(MeshGroupTest, TestMeshInstanceThreeArgumentConstructor) { + const auto mappings = MakeMappings(4); + MeshGroup::MeshInstance instance(MeshIndex(2), 3, mappings); + ASSERT_EQ(instance.mesh_index, MeshIndex(2)); + ASSERT_EQ(instance.material_index, 3); + ASSERT_EQ(instance.materials_variants_mappings, mappings); +} + +TEST(MeshGroupTest, TestMeshInstanceEqualsOperator) { + MeshGroup::MeshInstance instance_a(MeshIndex(2), 3, MakeMappings(4)); + MeshGroup::MeshInstance instance_b(MeshIndex(2), 3, MakeMappings(4)); + ASSERT_TRUE(instance_a == instance_b); + ASSERT_FALSE(instance_a != instance_b); + + MeshGroup::MeshInstance instance_c(MeshIndex(1), 3, MakeMappings(4)); + MeshGroup::MeshInstance instance_d(MeshIndex(2), 1, MakeMappings(4)); + MeshGroup::MeshInstance instance_e(MeshIndex(2), 3, MakeMappings(1)); + ASSERT_FALSE(instance_a == instance_c); + ASSERT_FALSE(instance_a == instance_d); + ASSERT_FALSE(instance_a == instance_e); + ASSERT_TRUE(instance_a != instance_c); + ASSERT_TRUE(instance_a != instance_d); + ASSERT_TRUE(instance_a != instance_e); +} + +TEST(MeshGroupTest, TestRemoveMeshInstanceWithNoOccurrences) { + // Test that no mesh instances are removed from mesh group when removing the + // instances by the base mesh index that is not in the mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.AddMeshInstance({MeshIndex(1), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(3), 0, {}}); + + // Try to remove mesh that is not in the mesh group. + mesh_group.RemoveMeshInstances(MeshIndex(2)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 2); + ASSERT_EQ(mesh_group.GetMeshInstance(0).mesh_index, MeshIndex(1)); + ASSERT_EQ(mesh_group.GetMeshInstance(1).mesh_index, MeshIndex(3)); +} + +TEST(MeshGroupTest, TestRemoveTheOnlyMeshInstance) { + // Test that the only mesh instance can be removed from mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + MeshGroup::MaterialsVariantsMapping mapping(70, {0, 1}); + mesh_group.AddMeshInstance({MeshIndex(7), 70, {mapping}}); + + // Remove a mesh instance. + mesh_group.RemoveMeshInstances(MeshIndex(7)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 0); +} + +TEST(MeshGroupTest, TestRemoveOneMeshInstances) { + // Test a mesh instance can be removed from mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.AddMeshInstance({MeshIndex(1), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(3), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(5), 0, {}}); + mesh_group.AddMeshInstance({MeshIndex(7), 0, {}}); + + // Remove a mesh. + mesh_group.RemoveMeshInstances(MeshIndex(3)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 3); + ASSERT_EQ(mesh_group.GetMeshInstance(0).mesh_index, MeshIndex(1)); + ASSERT_EQ(mesh_group.GetMeshInstance(1).mesh_index, MeshIndex(5)); + ASSERT_EQ(mesh_group.GetMeshInstance(2).mesh_index, MeshIndex(7)); +} + +TEST(MeshGroupTest, TestRemoveThreeMeshInstances) { + // Test that multiple mesh instances can be removed from a mesh group. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + mesh_group.AddMeshInstance({MeshIndex(3), 30, MakeMappings(3)}); + mesh_group.AddMeshInstance({MeshIndex(5), 50, MakeMappings(5)}); + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + mesh_group.AddMeshInstance({MeshIndex(7), 70, MakeMappings(7)}); + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + + // Remove mesh instances. + mesh_group.RemoveMeshInstances(MeshIndex(1)); + + // Check result. + ASSERT_EQ(mesh_group.NumMeshInstances(), 3); + const MeshGroup::MeshInstance mi0 = mesh_group.GetMeshInstance(0); + const MeshGroup::MeshInstance mi1 = mesh_group.GetMeshInstance(1); + const MeshGroup::MeshInstance mi2 = mesh_group.GetMeshInstance(2); + ASSERT_EQ(mi0.mesh_index, MeshIndex(3)); + ASSERT_EQ(mi1.mesh_index, MeshIndex(5)); + ASSERT_EQ(mi2.mesh_index, MeshIndex(7)); + ASSERT_EQ(mi0.material_index, 30); + ASSERT_EQ(mi1.material_index, 50); + ASSERT_EQ(mi2.material_index, 70); + ASSERT_EQ(mi0.materials_variants_mappings, MakeMappings(3)); + ASSERT_EQ(mi1.materials_variants_mappings, MakeMappings(5)); + ASSERT_EQ(mi2.materials_variants_mappings, MakeMappings(7)); +} + +TEST(MeshGroupTest, TestCopy) { + // Tests that a mesh group can be copied. + + // Create test mesh group. + MeshGroup mesh_group; + mesh_group.SetName("Mesh-1-3-5-7"); + mesh_group.AddMeshInstance({MeshIndex(1), 10, MakeMappings(1)}); + mesh_group.AddMeshInstance({MeshIndex(3), 30, MakeMappings(3)}); + mesh_group.AddMeshInstance({MeshIndex(5), 50, MakeMappings(5)}); + mesh_group.AddMeshInstance({MeshIndex(7), 70, MakeMappings(7)}); + + // Verify source MeshGroup. + ASSERT_EQ(mesh_group.GetName(), "Mesh-1-3-5-7"); + ASSERT_EQ(mesh_group.NumMeshInstances(), 4); + const MeshGroup::MeshInstance mi0 = mesh_group.GetMeshInstance(0); + const MeshGroup::MeshInstance mi1 = mesh_group.GetMeshInstance(1); + const MeshGroup::MeshInstance mi2 = mesh_group.GetMeshInstance(2); + const MeshGroup::MeshInstance mi3 = mesh_group.GetMeshInstance(3); + ASSERT_EQ(mi0.mesh_index, MeshIndex(1)); + ASSERT_EQ(mi1.mesh_index, MeshIndex(3)); + ASSERT_EQ(mi2.mesh_index, MeshIndex(5)); + ASSERT_EQ(mi3.mesh_index, MeshIndex(7)); + ASSERT_EQ(mi0.material_index, 10); + ASSERT_EQ(mi1.material_index, 30); + ASSERT_EQ(mi2.material_index, 50); + ASSERT_EQ(mi3.material_index, 70); + ASSERT_EQ(mi0.materials_variants_mappings, MakeMappings(1)); + ASSERT_EQ(mi1.materials_variants_mappings, MakeMappings(3)); + ASSERT_EQ(mi2.materials_variants_mappings, MakeMappings(5)); + ASSERT_EQ(mi3.materials_variants_mappings, MakeMappings(7)); + + MeshGroup copy; + copy.Copy(mesh_group); + + // Verify that Copy worked. + ASSERT_EQ(mesh_group.GetName(), copy.GetName()); + ASSERT_EQ(mesh_group.NumMeshInstances(), copy.NumMeshInstances()); + ASSERT_EQ(mesh_group.GetMeshInstance(0), copy.GetMeshInstance(0)); + ASSERT_EQ(mesh_group.GetMeshInstance(1), copy.GetMeshInstance(1)); + ASSERT_EQ(mesh_group.GetMeshInstance(2), copy.GetMeshInstance(2)); + ASSERT_EQ(mesh_group.GetMeshInstance(3), copy.GetMeshInstance(3)); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/scene.cc b/contrib/draco/src/draco/scene/scene.cc new file mode 100644 index 000000000..9ad574835 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene.cc @@ -0,0 +1,174 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/scene.h" + +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/macros.h" +#include "draco/scene/scene_indices.h" + +namespace draco { + +void Scene::Copy(const Scene &s) { + meshes_.resize(s.meshes_.size()); + for (MeshIndex i(0); i < meshes_.size(); ++i) { + meshes_[i] = std::unique_ptr(new Mesh()); + meshes_[i]->Copy(*s.meshes_[i]); + } + + mesh_groups_.resize(s.mesh_groups_.size()); + for (MeshGroupIndex i(0); i < mesh_groups_.size(); ++i) { + mesh_groups_[i] = std::unique_ptr(new MeshGroup()); + mesh_groups_[i]->Copy(*s.mesh_groups_[i]); + } + + nodes_.resize(s.nodes_.size()); + for (SceneNodeIndex i(0); i < nodes_.size(); ++i) { + nodes_[i] = std::unique_ptr(new SceneNode()); + nodes_[i]->Copy(*s.nodes_[i]); + } + + root_node_indices_ = s.root_node_indices_; + + animations_.resize(s.animations_.size()); + for (AnimationIndex i(0); i < animations_.size(); ++i) { + animations_[i] = std::unique_ptr(new Animation()); + animations_[i]->Copy(*s.animations_[i]); + } + + skins_.resize(s.skins_.size()); + for (SkinIndex i(0); i < skins_.size(); ++i) { + skins_[i] = std::unique_ptr(new Skin()); + skins_[i]->Copy(*s.skins_[i]); + } + + lights_.resize(s.lights_.size()); + for (LightIndex i(0); i < lights_.size(); ++i) { + lights_[i] = std::unique_ptr(new Light()); + lights_[i]->Copy(*s.lights_[i]); + } + + instance_arrays_.resize(s.instance_arrays_.size()); + for (InstanceArrayIndex i(0); i < instance_arrays_.size(); ++i) { + instance_arrays_[i] = std::unique_ptr(new InstanceArray()); + instance_arrays_[i]->Copy(*s.instance_arrays_[i]); + } + + material_library_.Copy(s.material_library_); + +#ifdef DRACO_TRANSCODER_SUPPORTED + // Copy non-material textures. + non_material_texture_library_.Copy(s.non_material_texture_library_); + + // Update pointers to non-material textures in mesh feature ID sets of all + // scene meshes. + if (non_material_texture_library_.NumTextures() != 0) { + const auto texture_to_index_map = + s.non_material_texture_library_.ComputeTextureToIndexMap(); + for (MeshIndex i(0); i < NumMeshes(); ++i) { + for (MeshFeaturesIndex j(0); j < GetMesh(i).NumMeshFeatures(); ++j) { + Mesh::UpdateMeshFeaturesTexturePointer(texture_to_index_map, + &non_material_texture_library_, + &GetMesh(i).GetMeshFeatures(j)); + } + } + } +#endif // DRACO_TRANSCODER_SUPPORTED + + // Copy structural metadata. + structural_metadata_.Copy(s.structural_metadata_); +} + +Status Scene::RemoveMesh(MeshIndex index) { + // Remove base mesh at |index| from |meshes_| and corresponding material index + // from |mesh_material_indices_|. + const int new_num_meshes = meshes_.size() - 1; + for (MeshIndex i(index); i < new_num_meshes; i++) { + meshes_[i] = std::move(meshes_[i + 1]); + } + meshes_.resize(new_num_meshes); + + // Remove references to removed base mesh and corresponding materials from + // mesh groups, and update references to remaining base meshes in mesh groups. + for (MeshGroupIndex mgi(0); mgi < NumMeshGroups(); ++mgi) { + MeshGroup *const mesh_group = GetMeshGroup(mgi); + if (!mesh_group) { + return Status(Status::DRACO_ERROR, "MeshGroup is null."); + } + mesh_group->RemoveMeshInstances(index); + for (int i = 0; i < mesh_group->NumMeshInstances(); ++i) { + MeshGroup::MeshInstance &mesh_instance = mesh_group->GetMeshInstance(i); + if (mesh_instance.mesh_index > index && + mesh_instance.mesh_index != kInvalidMeshIndex) { + mesh_instance.mesh_index--; + } + } + } + return OkStatus(); +} + +Status Scene::RemoveMeshGroup(MeshGroupIndex index) { + // Remove mesh group at |index| from |mesh_groups_| vector. + const int new_num_mesh_groups = mesh_groups_.size() - 1; + for (MeshGroupIndex i(index); i < new_num_mesh_groups; i++) { + mesh_groups_[i] = std::move(mesh_groups_[i + 1]); + } + mesh_groups_.resize(new_num_mesh_groups); + + // Invalidate references to removed mesh group in scene nodes, and update + // references to remaining mesh groups in scene nodes. + for (SceneNodeIndex sni(0); sni < NumNodes(); ++sni) { + SceneNode *node = GetNode(sni); + if (!node) { + return Status(Status::DRACO_ERROR, "Node is null."); + } + const MeshGroupIndex mgi = node->GetMeshGroupIndex(); + if (mgi == index) { + // TODO(vytyaz): Remove the node if possible, e.g., when node has no + // geometry, no child nodes, no skins, no lights, and no mesh group + // instance arrays. + node->SetMeshGroupIndex(kInvalidMeshGroupIndex); + } else if (mgi > index && mgi != kInvalidMeshGroupIndex) { + node->SetMeshGroupIndex(mgi - 1); + } + } + return OkStatus(); +} + +Status Scene::RemoveMaterial(int index) { + if (index < 0 || index >= material_library_.NumMaterials()) { + return Status(Status::DRACO_ERROR, "Material index is out of range."); + } + material_library_.RemoveMaterial(index); + + // Update material indices of mesh instances. + for (MeshGroupIndex mgi(0); mgi < NumMeshGroups(); ++mgi) { + MeshGroup *const mesh_group = GetMeshGroup(mgi); + for (int i = 0; i < mesh_group->NumMeshInstances(); i++) { + MeshGroup::MeshInstance &mesh_instance = mesh_group->GetMeshInstance(i); + if (mesh_instance.material_index > index) { + mesh_instance.material_index--; + } else if (mesh_instance.material_index == index) { + return Status(Status::DRACO_ERROR, "Removed material has references."); + } + } + } + return OkStatus(); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene.h b/contrib/draco/src/draco/scene/scene.h new file mode 100644 index 000000000..3c76ead7a --- /dev/null +++ b/contrib/draco/src/draco/scene/scene.h @@ -0,0 +1,258 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_SCENE_SCENE_H_ +#define DRACO_SCENE_SCENE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/animation/animation.h" +#include "draco/animation/skin.h" +#include "draco/mesh/mesh.h" +#include "draco/metadata/structural_metadata.h" +#include "draco/scene/instance_array.h" +#include "draco/scene/light.h" +#include "draco/scene/mesh_group.h" +#include "draco/scene/scene_indices.h" +#include "draco/scene/scene_node.h" + +namespace draco { + +// Class used to hold all of the geometry to create a scene. A scene is +// comprised of one or more meshes, one or more scene nodes, one or more +// mesh groups, and a material library. The meshes are defined in their +// local space. A mesh group is a list of meshes. The scene nodes create +// a scene hierarchy to transform meshes in their local space into scene space. +// The material library contains all of the materials and textures used by the +// meshes in this scene. +class Scene { + public: + Scene() {} + + void Copy(const Scene &s); + + // Adds a Draco |mesh| to the scene. Returns the index to the stored mesh or + // |kInvalidMeshIndex| if the mesh is a nullptr. + MeshIndex AddMesh(std::unique_ptr mesh) { + if (mesh == nullptr) { + return kInvalidMeshIndex; + } + meshes_.push_back(std::move(mesh)); + return MeshIndex(meshes_.size() - 1); + } + + // Removes base mesh and corresponding material at |index|, removes references + // to removed base mesh and corresponding materials from mesh groups, and + // updates references to remaining base meshes in mesh groups. + Status RemoveMesh(MeshIndex index); + + // Returns the number of meshes in a scene before instancing is applied. + int NumMeshes() const { return meshes_.size(); } + + // Returns a mesh in the scene before instancing is applied. The mesh + // coordinates are local to the mesh. + Mesh &GetMesh(MeshIndex index) { return *meshes_[index]; } + const Mesh &GetMesh(MeshIndex index) const { return *meshes_[index]; } + + // Creates a mesh group and returns the index to the mesh group. + MeshGroupIndex AddMeshGroup() { + std::unique_ptr mesh(new MeshGroup()); + mesh_groups_.push_back(std::move(mesh)); + return MeshGroupIndex(mesh_groups_.size() - 1); + } + + // Removes mesh group at |index|, invalidates references to removed mesh group + // in scene nodes, and updates references to remaining mesh groups in scene + // nodes. + Status RemoveMeshGroup(MeshGroupIndex index); + + // Removes unused material at |index| and updates references to materials at + // indices greater than |index|. Returns error status when |index| is out of + // valid range and when material at |index| is used in the scene. + Status RemoveMaterial(int index); + + // Returns the number of mesh groups in a scene. + int NumMeshGroups() const { return mesh_groups_.size(); } + + // Returns a mesh group in the scene. + MeshGroup *GetMeshGroup(MeshGroupIndex index) { + return mesh_groups_[index].get(); + } + const MeshGroup *GetMeshGroup(MeshGroupIndex index) const { + return mesh_groups_[index].get(); + } + + // Creates a scene node and returns the index to the node. + SceneNodeIndex AddNode() { + std::unique_ptr node(new SceneNode()); + nodes_.push_back(std::move(node)); + return SceneNodeIndex(nodes_.size() - 1); + } + + // Returns the number of nodes in a scene. + int NumNodes() const { return nodes_.size(); } + + // Returns a node in the scene. + SceneNode *GetNode(SceneNodeIndex index) { return nodes_[index].get(); } + const SceneNode *GetNode(SceneNodeIndex index) const { + return nodes_[index].get(); + } + + // Either allocates new nodes or removes existing nodes that are beyond + // |num_nodes|. + void ResizeNodes(int num_nodes) { + const size_t old_num_nodes = nodes_.size(); + nodes_.resize(num_nodes); + for (SceneNodeIndex i(old_num_nodes); i < num_nodes; ++i) { + nodes_[i].reset(new SceneNode()); + } + } + + // Returns the number of root node indices in a scene. + int NumRootNodes() const { return root_node_indices_.size(); } + SceneNodeIndex GetRootNodeIndex(int i) const { return root_node_indices_[i]; } + const std::vector &GetRootNodeIndices() const { + return root_node_indices_; + } + void AddRootNodeIndex(SceneNodeIndex index) { + root_node_indices_.push_back(index); + } + void SetRootNodeIndex(int i, SceneNodeIndex index) { + root_node_indices_[i] = index; + } + void RemoveAllRootNodeIndices() { root_node_indices_.clear(); } + + const MaterialLibrary &GetMaterialLibrary() const { + return material_library_; + } + MaterialLibrary &GetMaterialLibrary() { return material_library_; } + + // Library that contains non-material textures. + const TextureLibrary &GetNonMaterialTextureLibrary() const { + return non_material_texture_library_; + } + TextureLibrary &GetNonMaterialTextureLibrary() { + return non_material_texture_library_; + } + + // Structural metadata. + const StructuralMetadata &GetStructuralMetadata() const { + return structural_metadata_; + } + StructuralMetadata &GetStructuralMetadata() { return structural_metadata_; } + + // Creates an animation and returns the index to the animation. + AnimationIndex AddAnimation() { + std::unique_ptr animation(new Animation()); + animations_.push_back(std::move(animation)); + return AnimationIndex(animations_.size() - 1); + } + + // Returns the number of animations in a scene. + int NumAnimations() const { return animations_.size(); } + + // Returns an animation in the scene. + Animation *GetAnimation(AnimationIndex index) { + return animations_[index].get(); + } + const Animation *GetAnimation(AnimationIndex index) const { + return animations_[index].get(); + } + + // Creates a skin and returns the index to the skin. + SkinIndex AddSkin() { + std::unique_ptr skin(new Skin()); + skins_.push_back(std::move(skin)); + return SkinIndex(skins_.size() - 1); + } + + // Returns the number of skins in a scene. + int NumSkins() const { return skins_.size(); } + + // Returns a skin in the scene. + Skin *GetSkin(SkinIndex index) { return skins_[index].get(); } + const Skin *GetSkin(SkinIndex index) const { return skins_[index].get(); } + + // Creates a light and returns the index to the light. + LightIndex AddLight() { + std::unique_ptr light(new Light()); + lights_.push_back(std::move(light)); + return LightIndex(lights_.size() - 1); + } + + // Returns the number of lights in a scene. + int NumLights() const { return lights_.size(); } + + // Returns a light in the scene. + Light *GetLight(LightIndex index) { return lights_[index].get(); } + const Light *GetLight(LightIndex index) const { return lights_[index].get(); } + + // Creates a mesh group instance array and returns the index to it. This array + // is used for storing the attributes of the EXT_mesh_gpu_instancing glTF + // extension. + InstanceArrayIndex AddInstanceArray() { + std::unique_ptr array(new InstanceArray()); + instance_arrays_.push_back(std::move(array)); + return InstanceArrayIndex(instance_arrays_.size() - 1); + } + + // Returns the number of mesh group instance arrays in a scene. + int NumInstanceArrays() const { return instance_arrays_.size(); } + + // Returns a mesh group instance array in the scene. + InstanceArray *GetInstanceArray(InstanceArrayIndex index) { + return instance_arrays_[index].get(); + } + const InstanceArray *GetInstanceArray(InstanceArrayIndex index) const { + return instance_arrays_[index].get(); + } + + private: + IndexTypeVector> meshes_; + IndexTypeVector> mesh_groups_; + IndexTypeVector> nodes_; + std::vector root_node_indices_; + IndexTypeVector> animations_; + IndexTypeVector> skins_; + + // The lights will be written to the output scene but not used for internal + // rendering in Draco, e.g, while computing distortion metric. + IndexTypeVector> lights_; + + // The mesh group instance array information will be written to the output + // scene but not processed by Draco simplifier modules. + IndexTypeVector> + instance_arrays_; + + // Materials used by this scene. + MaterialLibrary material_library_; + + // Texture library for storing non-material textures used by this scene, e.g., + // textures containing mesh feature IDs of EXT_mesh_features glTF extension. + // Note that scene meshes contain pointers to non-material textures. It is + // responsibility of class user to update these pointers when updating the + // textures. See Scene::Copy() for example. + TextureLibrary non_material_texture_library_; + + // Structural metadata defined by the EXT_structural_metadata glTF extension. + StructuralMetadata structural_metadata_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_SCENE_H_ diff --git a/contrib/draco/src/draco/scene/scene_are_equivalent.cc b/contrib/draco/src/draco/scene/scene_are_equivalent.cc new file mode 100644 index 000000000..7d0663e08 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_are_equivalent.cc @@ -0,0 +1,109 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/scene_are_equivalent.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/mesh/mesh_are_equivalent.h" + +namespace draco { + +bool SceneAreEquivalent::operator()(const Scene &scene0, const Scene &scene1) { + // Check scene component sizes. + if (scene0.NumAnimations() != scene1.NumAnimations()) { + return false; + } + if (scene0.NumMeshGroups() != scene1.NumMeshGroups()) { + return false; + } + if (scene0.NumSkins() != scene1.NumSkins()) { + return false; + } + + // Check equivalence of each mesh. + if (scene0.NumMeshes() != scene1.NumMeshes()) { + return false; + } + for (MeshIndex i(0); i < scene0.NumMeshes(); i++) { + if (!AreEquivalent(scene0.GetMesh(i), scene1.GetMesh(i))) { + return false; + } + } + + // Check eqiuvalence of each node. + if (scene0.NumNodes() != scene1.NumNodes()) { + return false; + } + for (SceneNodeIndex i(0); i < scene0.NumNodes(); i++) { + if (!AreEquivalent(*scene0.GetNode(i), *scene1.GetNode(i))) { + return false; + } + } + + // Check non-material texture library sizes. + if (scene0.GetNonMaterialTextureLibrary().NumTextures() != + scene1.GetNonMaterialTextureLibrary().NumTextures()) { + return false; + } + + // TODO(vytyaz): Check remaining scene properties like animations and skins. + return true; +} + +bool SceneAreEquivalent::AreEquivalent(const Mesh &mesh0, const Mesh &mesh1) { + MeshAreEquivalent eq; + return eq(mesh0, mesh1); +} + +bool SceneAreEquivalent::AreEquivalent(const SceneNode &node0, + const SceneNode &node1) { + // Check equivalence of node indices. + if (node0.GetMeshGroupIndex() != node1.GetMeshGroupIndex()) { + return false; + } + if (node0.GetSkinIndex() != node1.GetSkinIndex()) { + return false; + } + + // Check equivalence of node transformations. + if (node0.GetTrsMatrix().ComputeTransformationMatrix() != + node1.GetTrsMatrix().ComputeTransformationMatrix()) { + return false; + } + + // Check equivalence of node children. + if (node0.NumChildren() != node1.NumChildren()) { + return false; + } + for (int i = 0; i < node0.NumChildren(); i++) { + if (node0.Child(i) != node1.Child(i)) { + return false; + } + } + + // Check equivalence of node parents. + if (node0.NumParents() != node1.NumParents()) { + return false; + } + for (int i = 0; i < node0.NumParents(); i++) { + if (node0.Parent(i) != node1.Parent(i)) { + return false; + } + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_are_equivalent.h b/contrib/draco/src/draco/scene/scene_are_equivalent.h new file mode 100644 index 000000000..b309c0338 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_are_equivalent.h @@ -0,0 +1,42 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#ifndef DRACO_SCENE_SCENE_ARE_EQUIVALENT_H_ +#define DRACO_SCENE_SCENE_ARE_EQUIVALENT_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/scene/scene.h" + +namespace draco { + +// A functor to compare two scenes for equivalency up to permutation of mesh +// vertices. +class SceneAreEquivalent { + public: + // Returns true if both scenes are equivalent up to permutation of + // the internal order of mesh vertices. This includes all attributes. + bool operator()(const Scene &scene0, const Scene &scene1); + + private: + static bool AreEquivalent(const Mesh &mesh0, const Mesh &mesh1); + static bool AreEquivalent(const SceneNode &node0, const SceneNode &node1); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_SCENE_ARE_EQUIVALENT_H_ diff --git a/contrib/draco/src/draco/scene/scene_are_equivalent_test.cc b/contrib/draco/src/draco/scene/scene_are_equivalent_test.cc new file mode 100644 index 000000000..3a9edc6c3 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_are_equivalent_test.cc @@ -0,0 +1,86 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/scene_are_equivalent.h" + +#include +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/scene_io.h" +#include "draco/scene/scene.h" + +namespace draco { + +#ifdef DRACO_TRANSCODER_SUPPORTED +class SceneAreEquivalentTest : public ::testing::Test {}; + +TEST_F(SceneAreEquivalentTest, TestOnIndenticalScenes) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene(ReadSceneFromTestFile(file_name)); + ASSERT_NE(scene, nullptr) << "Failed to load test scene: " << file_name; + + // Add mesh feature ID set to a scene mesh. + std::unique_ptr mesh_features(new MeshFeatures()); + scene->GetMesh(MeshIndex(2)).AddMeshFeatures(std::move(mesh_features)); + + SceneAreEquivalent equiv; + ASSERT_TRUE(equiv(*scene, *scene)); +} + +TEST_F(SceneAreEquivalentTest, TestOnDifferentScenes) { + const std::string file_name0 = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::string file_name1 = "Lantern/glTF/Lantern.gltf"; + const std::unique_ptr scene0(ReadSceneFromTestFile(file_name0)); + const std::unique_ptr scene1(ReadSceneFromTestFile(file_name1)); + ASSERT_NE(scene0, nullptr) << "Failed to load test scene: " << file_name0; + ASSERT_NE(scene1, nullptr) << "Failed to load test scene: " << file_name1; + SceneAreEquivalent equiv; + ASSERT_FALSE(equiv(*scene0, *scene1)); +} + +TEST_F(SceneAreEquivalentTest, TestMeshFeatures) { + const std::string file_name = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + const std::unique_ptr scene0(ReadSceneFromTestFile(file_name)); + const std::unique_ptr scene1(ReadSceneFromTestFile(file_name)); + ASSERT_NE(scene0, nullptr); + ASSERT_NE(scene1, nullptr); + + // Add identical mesh feature ID sets to mesh at index 0. + Mesh &mesh0 = scene0->GetMesh(MeshIndex(0)); + Mesh &mesh1 = scene1->GetMesh(MeshIndex(0)); + mesh0.AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + mesh1.AddMeshFeatures(std::unique_ptr(new MeshFeatures())); + + // Empty feature sets should match. + SceneAreEquivalent equiv; + ASSERT_TRUE(equiv(*scene0, *scene1)); + + // Make mesh features different and check that the meshes are not equivalent. + mesh0.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(5); + mesh1.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(6); + ASSERT_FALSE(equiv(*scene0, *scene1)); + + // Make mesh features identical and check that the meshes are equivalent. + mesh0.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + mesh1.GetMeshFeatures(MeshFeaturesIndex(0)).SetFeatureCount(1); + ASSERT_TRUE(equiv(*scene0, *scene1)); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace draco diff --git a/contrib/draco/src/draco/scene/scene_indices.h b/contrib/draco/src/draco/scene/scene_indices.h new file mode 100644 index 000000000..7b57e3e4a --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_indices.h @@ -0,0 +1,72 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifdef DRACO_TRANSCODER_SUPPORTED +#ifndef DRACO_SCENE_SCENE_INDICES_H_ +#define DRACO_SCENE_SCENE_INDICES_H_ + +#include + +#include + +#include "draco/core/draco_index_type.h" + +namespace draco { + +// Index of a mesh in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshIndex) + +// Index of a mesh instance in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshInstanceIndex) + +// Index of a mesh group in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, MeshGroupIndex) + +// Index of a node in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, SceneNodeIndex) + +// Index of an animation in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, AnimationIndex) + +// Index of a skin in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, SkinIndex) + +// Index of a light in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, LightIndex) + +// Index of a mesh group GPU instancing in a scene. +DEFINE_NEW_DRACO_INDEX_TYPE(uint32_t, InstanceArrayIndex) + +// Constants denoting invalid indices. +static constexpr MeshIndex kInvalidMeshIndex( + std::numeric_limits::max()); +static constexpr MeshInstanceIndex kInvalidMeshInstanceIndex( + std::numeric_limits::max()); +static constexpr MeshGroupIndex kInvalidMeshGroupIndex( + std::numeric_limits::max()); +static constexpr SceneNodeIndex kInvalidSceneNodeIndex( + std::numeric_limits::max()); +static constexpr AnimationIndex kInvalidAnimationIndex( + std::numeric_limits::max()); +static constexpr SkinIndex kInvalidSkinIndex( + std::numeric_limits::max()); +static constexpr LightIndex kInvalidLightIndex( + std::numeric_limits::max()); +static constexpr InstanceArrayIndex kInvalidInstanceArrayIndex( + std::numeric_limits::max()); + +} // namespace draco + +#endif // DRACO_SCENE_SCENE_INDICES_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_node.h b/contrib/draco/src/draco/scene/scene_node.h new file mode 100644 index 000000000..6cfe04e2e --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_node.h @@ -0,0 +1,105 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifdef DRACO_TRANSCODER_SUPPORTED +#ifndef DRACO_SCENE_SCENE_NODE_H_ +#define DRACO_SCENE_SCENE_NODE_H_ + +#include "draco/scene/scene_indices.h" +#include "draco/scene/trs_matrix.h" + +namespace draco { + +// This class is used to create a scene hierarchy from meshes in their local +// space transformed into scene space. +class SceneNode { + public: + SceneNode() + : mesh_group_index_(-1), + skin_index_(-1), + light_index_(-1), + instance_array_index_(-1) {} + + void Copy(const SceneNode &sn) { + name_ = sn.name_; + trs_matrix_.Copy(sn.trs_matrix_); + mesh_group_index_ = sn.mesh_group_index_; + skin_index_ = sn.skin_index_; + parents_ = sn.parents_; + children_ = sn.children_; + light_index_ = sn.light_index_; + instance_array_index_ = sn.instance_array_index_; + } + + // Sets a name. + void SetName(const std::string &name) { name_ = name; } + + // Returns the name. + const std::string &GetName() const { return name_; } + + // Set transformation from mesh local space to scene space. + void SetTrsMatrix(const TrsMatrix &trsm) { trs_matrix_.Copy(trsm); } + const TrsMatrix &GetTrsMatrix() const { return trs_matrix_; } + + // Set the index to the mesh group in the scene. + void SetMeshGroupIndex(MeshGroupIndex index) { mesh_group_index_ = index; } + MeshGroupIndex GetMeshGroupIndex() const { return mesh_group_index_; } + + // Set the index to the skin in the scene. + void SetSkinIndex(SkinIndex index) { skin_index_ = index; } + SkinIndex GetSkinIndex() const { return skin_index_; } + + // Set the index to the light in the scene. + void SetLightIndex(LightIndex index) { light_index_ = index; } + LightIndex GetLightIndex() const { return light_index_; } + + // Set the index to the mesh group instance array in the scene. Note that + // according to EXT_mesh_gpu_instancing glTF extension there is no defined + // behavior for a node with instance array and without a mesh group. + void SetInstanceArrayIndex(InstanceArrayIndex index) { + instance_array_index_ = index; + } + InstanceArrayIndex GetInstanceArrayIndex() const { + return instance_array_index_; + } + + // Functions to set and get zero or more parent nodes of this node. + SceneNodeIndex Parent(int index) const { return parents_[index]; } + const std::vector &Parents() const { return parents_; } + void AddParentIndex(SceneNodeIndex index) { parents_.push_back(index); } + int NumParents() const { return parents_.size(); } + void RemoveAllParents() { parents_.clear(); } + + // Functions to set and get zero or more child nodes of this node. + SceneNodeIndex Child(int index) const { return children_[index]; } + const std::vector &Children() const { return children_; } + void AddChildIndex(SceneNodeIndex index) { children_.push_back(index); } + int NumChildren() const { return children_.size(); } + void RemoveAllChildren() { children_.clear(); } + + private: + std::string name_; + TrsMatrix trs_matrix_; + draco::MeshGroupIndex mesh_group_index_; + draco::SkinIndex skin_index_; + std::vector parents_; + std::vector children_; + LightIndex light_index_; + InstanceArrayIndex instance_array_index_; +}; + +} // namespace draco + +#endif // DRACO_SCENE_SCENE_NODE_H_ +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_test.cc b/contrib/draco/src/draco/scene/scene_test.cc new file mode 100644 index 000000000..d639614c7 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_test.cc @@ -0,0 +1,295 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/scene.h" + +#include +#include +#include + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/core/status.h" +#include "draco/mesh/mesh_are_equivalent.h" +#include "draco/scene/scene_are_equivalent.h" +#include "draco/scene/scene_indices.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +// Helper method for adding mesh group GPU instancing to the milk truck scene. +draco::Status AddGpuInstancingToMilkTruck(draco::Scene *scene) { + // Create an instance and set its transformation TRS vectors. + draco::InstanceArray::Instance instance_0; + instance_0.trs.SetTranslation(Eigen::Vector3d(1.0, 2.0, 3.0)); + instance_0.trs.SetRotation(Eigen::Quaterniond(4.0, 5.0, 6.0, 7.0)); + instance_0.trs.SetScale(Eigen::Vector3d(8.0, 9.0, 10.0)); + + // Create another instance. + draco::InstanceArray::Instance instance_1; + instance_1.trs.SetTranslation(Eigen::Vector3d(1.1, 2.1, 3.1)); + instance_1.trs.SetRotation(Eigen::Quaterniond(4.1, 5.1, 6.1, 7.1)); + instance_1.trs.SetScale(Eigen::Vector3d(8.1, 9.1, 10.1)); + + // Add an empty GPU instancing object to the scene. + const draco::InstanceArrayIndex index = scene->AddInstanceArray(); + draco::InstanceArray *gpu_instancing = scene->GetInstanceArray(index); + + // Add two instances to the GPU instancing object stored in the scene. + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_0)); + DRACO_RETURN_IF_ERROR(gpu_instancing->AddInstance(instance_1)); + + // Assign the GPU instancing object to two mesh groups in two scene nodes. + scene->GetNode(draco::SceneNodeIndex(2))->SetInstanceArrayIndex(index); + scene->GetNode(draco::SceneNodeIndex(4))->SetInstanceArrayIndex(index); + + return draco::OkStatus(); +} + +TEST(SceneTest, TestCopy) { + // Test copying of scene data. + auto src_scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene, nullptr); + + // Add GPU instancing to the scene for testing. + DRACO_ASSERT_OK(AddGpuInstancingToMilkTruck(src_scene.get())); + ASSERT_EQ(src_scene->NumInstanceArrays(), 1); + ASSERT_EQ(src_scene->NumNodes(), 5); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(0))->GetInstanceArrayIndex(), + draco::kInvalidInstanceArrayIndex); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(1))->GetInstanceArrayIndex(), + draco::kInvalidInstanceArrayIndex); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(2))->GetInstanceArrayIndex(), + draco::InstanceArrayIndex(0)); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(3))->GetInstanceArrayIndex(), + draco::kInvalidInstanceArrayIndex); + ASSERT_EQ( + src_scene->GetNode(draco::SceneNodeIndex(4))->GetInstanceArrayIndex(), + draco::InstanceArrayIndex(0)); + + // Make a copy of the scene. + draco::Scene dst_scene; + dst_scene.Copy(*src_scene); + + ASSERT_EQ(src_scene->NumMeshes(), dst_scene.NumMeshes()); + ASSERT_EQ(src_scene->NumMeshGroups(), dst_scene.NumMeshGroups()); + ASSERT_EQ(src_scene->NumNodes(), dst_scene.NumNodes()); + ASSERT_EQ(src_scene->NumAnimations(), dst_scene.NumAnimations()); + ASSERT_EQ(src_scene->NumSkins(), dst_scene.NumSkins()); + ASSERT_EQ(src_scene->NumLights(), dst_scene.NumLights()); + ASSERT_EQ(src_scene->NumInstanceArrays(), dst_scene.NumInstanceArrays()); + + for (draco::MeshIndex i(0); i < src_scene->NumMeshes(); ++i) { + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(src_scene->GetMesh(i), dst_scene.GetMesh(i))); + } + for (draco::MeshGroupIndex i(0); i < src_scene->NumMeshGroups(); ++i) { + ASSERT_EQ(src_scene->GetMeshGroup(i)->NumMeshInstances(), + dst_scene.GetMeshGroup(i)->NumMeshInstances()); + for (int j = 0; j < src_scene->GetMeshGroup(i)->NumMeshInstances(); ++j) { + ASSERT_EQ(src_scene->GetMeshGroup(i)->GetMeshInstance(j).mesh_index, + dst_scene.GetMeshGroup(i)->GetMeshInstance(j).mesh_index); + ASSERT_EQ(src_scene->GetMeshGroup(i)->GetMeshInstance(j).material_index, + dst_scene.GetMeshGroup(i)->GetMeshInstance(j).material_index); + ASSERT_EQ(src_scene->GetMeshGroup(i) + ->GetMeshInstance(j) + .materials_variants_mappings.size(), + dst_scene.GetMeshGroup(i) + ->GetMeshInstance(j) + .materials_variants_mappings.size()); + } + } + for (draco::SceneNodeIndex i(0); i < src_scene->NumNodes(); ++i) { + ASSERT_EQ(src_scene->GetNode(i)->NumParents(), + dst_scene.GetNode(i)->NumParents()); + for (int j = 0; j < src_scene->GetNode(i)->NumParents(); ++j) { + ASSERT_EQ(src_scene->GetNode(i)->Parent(j), + dst_scene.GetNode(i)->Parent(j)); + } + ASSERT_EQ(src_scene->GetNode(i)->NumChildren(), + dst_scene.GetNode(i)->NumChildren()); + for (int j = 0; j < src_scene->GetNode(i)->NumChildren(); ++j) { + ASSERT_EQ(src_scene->GetNode(i)->Child(j), + dst_scene.GetNode(i)->Child(j)); + } + ASSERT_EQ(src_scene->GetNode(i)->GetMeshGroupIndex(), + dst_scene.GetNode(i)->GetMeshGroupIndex()); + ASSERT_EQ(src_scene->GetNode(i)->GetSkinIndex(), + dst_scene.GetNode(i)->GetSkinIndex()); + ASSERT_EQ(src_scene->GetNode(i)->GetLightIndex(), + dst_scene.GetNode(i)->GetLightIndex()); + ASSERT_EQ(src_scene->GetNode(i)->GetInstanceArrayIndex(), + dst_scene.GetNode(i)->GetInstanceArrayIndex()); + } +} + +TEST(SceneTest, TestRemoveMesh) { + // Test that a base mesh can be removed from scene. + auto src_scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene_ptr, nullptr); + const draco::Scene &src_scene = *src_scene_ptr; + + // Copy scene. + draco::Scene dst_scene; + dst_scene.Copy(src_scene); + ASSERT_EQ(dst_scene.NumMeshes(), 4); + draco::MeshAreEquivalent eq; + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(0)), + src_scene.GetMesh(draco::MeshIndex(0)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(1)), + src_scene.GetMesh(draco::MeshIndex(1)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(2)), + src_scene.GetMesh(draco::MeshIndex(2)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(3)), + src_scene.GetMesh(draco::MeshIndex(3)))); + + // Remove base mesh from scene. + dst_scene.RemoveMesh(draco::MeshIndex(2)); + ASSERT_EQ(dst_scene.NumMeshes(), 3); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(0)), + src_scene.GetMesh(draco::MeshIndex(0)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(1)), + src_scene.GetMesh(draco::MeshIndex(1)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(2)), + src_scene.GetMesh(draco::MeshIndex(3)))); + + // Remove another base mesh from scene. + dst_scene.RemoveMesh(draco::MeshIndex(1)); + ASSERT_EQ(dst_scene.NumMeshes(), 2); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(0)), + src_scene.GetMesh(draco::MeshIndex(0)))); + ASSERT_TRUE(eq(dst_scene.GetMesh(draco::MeshIndex(1)), + src_scene.GetMesh(draco::MeshIndex(3)))); +} + +TEST(SceneTest, TestRemoveMeshGroup) { + // Test that a mesh group can be removed from scene. + auto src_scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene_ptr, nullptr); + const draco::Scene &src_scene = *src_scene_ptr; + + // Copy scene. + draco::Scene dst_scene; + dst_scene.Copy(src_scene); + ASSERT_EQ(dst_scene.NumMeshGroups(), 2); + ASSERT_EQ(dst_scene.NumNodes(), 5); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(2))->GetMeshGroupIndex(), + draco::MeshGroupIndex(1)); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(4))->GetMeshGroupIndex(), + draco::MeshGroupIndex(1)); + + // Remove mesh group from scene. + dst_scene.RemoveMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.NumMeshGroups(), 1); + ASSERT_EQ(dst_scene.NumNodes(), 5); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(2))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(4))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + + // Remove another mesh group from scene. + dst_scene.RemoveMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(dst_scene.NumMeshGroups(), 0); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(2))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); + ASSERT_EQ(dst_scene.GetNode(draco::SceneNodeIndex(4))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); +} + +void CheckMeshMaterials(const draco::Scene &scene, + const std::vector &expected_material_indices) { + ASSERT_EQ(scene.NumMeshes(), expected_material_indices.size()); + std::vector scene_material_indices; + for (draco::MeshGroupIndex i(0); i < scene.NumMeshGroups(); i++) { + const auto mg = scene.GetMeshGroup(i); + for (int mi = 0; mi < mg->NumMeshInstances(); ++mi) { + scene_material_indices.push_back(mg->GetMeshInstance(mi).material_index); + } + } + ASSERT_EQ(scene_material_indices, expected_material_indices); +} + +TEST(SceneTest, TestRemoveMaterial) { + // Test that materials can be removed from a scene. + auto src_scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(src_scene_ptr, nullptr); + const draco::Scene &src_scene = *src_scene_ptr; + ASSERT_EQ(src_scene.GetMaterialLibrary().NumMaterials(), 4); + CheckMeshMaterials(src_scene, {0, 1, 2, 3}); + + // Copy scene. + draco::Scene dst_scene; + dst_scene.Copy(src_scene); + + // Check that referenced material cannot be removed from the scene. + ASSERT_FALSE(dst_scene.RemoveMaterial(2).ok()); + + // Copy scene again, since failed material removal corrupts the scene. + dst_scene.Copy(src_scene); + + // Remove base mesh from scene. Material at index 2 becomes unreferenced. + DRACO_ASSERT_OK(dst_scene.RemoveMesh(draco::MeshIndex(2))); + ASSERT_EQ(dst_scene.GetMaterialLibrary().NumMaterials(), 4); + CheckMeshMaterials(dst_scene, {0, 1, 3}); + + // Check that unreferenced material can be removed from the scene. + DRACO_ASSERT_OK(dst_scene.RemoveMaterial(2)); + ASSERT_EQ(dst_scene.GetMaterialLibrary().NumMaterials(), 3); + CheckMeshMaterials(dst_scene, {0, 1, 2}); + + // Check that material cannot be removed when material index is out of range. + ASSERT_FALSE(dst_scene.RemoveMaterial(-1).ok()); + ASSERT_FALSE(dst_scene.RemoveMaterial(3).ok()); +} + +TEST(SceneTest, TestCopyWithStructuralMetadata) { + // Tests copying of a scene with structural metadata. + auto scene_ptr = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene_ptr, nullptr); + draco::Scene &scene = *scene_ptr; + + // Add structural metadata to the scene. + draco::PropertyTable::Schema schema; + schema.json.SetString("Data"); + scene.GetStructuralMetadata().SetPropertyTableSchema(schema); + + // Copy the scene. + draco::Scene copy; + copy.Copy(scene); + + // Check that the structural metadata has been copied. + ASSERT_EQ( + copy.GetStructuralMetadata().GetPropertyTableSchema().json.GetString(), + "Data"); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/scene/scene_utils.cc b/contrib/draco/src/draco/scene/scene_utils.cc new file mode 100644 index 000000000..a7bf1dcb9 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_utils.cc @@ -0,0 +1,962 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#include "draco/scene/scene_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include + +#include "draco/core/draco_index_type_vector.h" +#include "draco/core/hash_utils.h" +#include "draco/core/vector_d.h" +#include "draco/mesh/mesh_splitter.h" +#include "draco/mesh/mesh_utils.h" +#include "draco/scene/scene_indices.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +IndexTypeVector +SceneUtils::ComputeAllInstances(const Scene &scene) { + IndexTypeVector instances; + + // Traverse the scene assuming multiple root nodes. + const Eigen::Matrix4d transform = Eigen::Matrix4d::Identity(); + + struct Node { + const SceneNodeIndex scene_node_index; + Eigen::Matrix4d transform; + }; + std::vector nodes; + nodes.reserve(scene.NumRootNodes()); + for (int i = 0; i < scene.NumRootNodes(); ++i) { + nodes.push_back({scene.GetRootNodeIndex(i), transform}); + } + + while (!nodes.empty()) { + const Node node = nodes.back(); + nodes.pop_back(); + const SceneNode &scene_node = *scene.GetNode(node.scene_node_index); + const Eigen::Matrix4d combined_transform = + node.transform * + scene_node.GetTrsMatrix().ComputeTransformationMatrix(); + + // Create instances from node meshes. + const MeshGroupIndex mesh_group_index = scene_node.GetMeshGroupIndex(); + if (mesh_group_index != kInvalidMeshGroupIndex) { + const MeshGroup &mesh_group = *scene.GetMeshGroup(mesh_group_index); + for (int i = 0; i < mesh_group.NumMeshInstances(); i++) { + const MeshIndex mesh_index = mesh_group.GetMeshInstance(i).mesh_index; + if (mesh_index != kInvalidMeshIndex) { + instances.push_back( + {mesh_index, node.scene_node_index, i, combined_transform}); + } + } + } + + // Traverse children nodes. + for (int i = 0; i < scene_node.NumChildren(); i++) { + nodes.push_back({scene_node.Child(i), combined_transform}); + } + } + return instances; +} + +Eigen::Matrix4d SceneUtils::ComputeGlobalNodeTransform(const Scene &scene, + SceneNodeIndex index) { + Eigen::Matrix4d transform = Eigen::Matrix4d::Identity(); + while (index != kInvalidSceneNodeIndex) { + const SceneNode *const node = scene.GetNode(index); + transform = node->GetTrsMatrix().ComputeTransformationMatrix() * transform; + index = node->NumParents() == 1 ? node->Parent(0) : kInvalidSceneNodeIndex; + } + return transform; +} + +IndexTypeVector SceneUtils::NumMeshInstances( + const Scene &scene) { + const auto instances = ComputeAllInstances(scene); + IndexTypeVector num_mesh_instances(scene.NumMeshes(), 0); + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + num_mesh_instances[instance.mesh_index]++; + } + return num_mesh_instances; +} + +int SceneUtils::GetMeshInstanceMaterialIndex(const Scene &scene, + const MeshInstance &instance) { + const auto *const node = scene.GetNode(instance.scene_node_index); + return scene.GetMeshGroup(node->GetMeshGroupIndex()) + ->GetMeshInstance(instance.mesh_group_mesh_index) + .material_index; +} + +int SceneUtils::NumFacesOnBaseMeshes(const Scene &scene) { + int num_faces = 0; + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + num_faces += scene.GetMesh(i).num_faces(); + } + return num_faces; +} + +int SceneUtils::NumFacesOnInstancedMeshes(const Scene &scene) { + const auto instances = ComputeAllInstances(scene); + int num_faces = 0; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + num_faces += scene.GetMesh(instance.mesh_index).num_faces(); + } + return num_faces; +} + +int SceneUtils::NumPointsOnBaseMeshes(const Scene &scene) { + int num_points = 0; + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + num_points += scene.GetMesh(i).num_points(); + } + return num_points; +} + +int SceneUtils::NumPointsOnInstancedMeshes(const Scene &scene) { + const auto instances = ComputeAllInstances(scene); + int num_points = 0; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + num_points += scene.GetMesh(instance.mesh_index).num_points(); + } + return num_points; +} + +int SceneUtils::NumAttEntriesOnBaseMeshes(const Scene &scene, + GeometryAttribute::Type att_type) { + int num_att_entries = 0; + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + const Mesh &mesh = scene.GetMesh(i); + const PointAttribute *att = mesh.GetNamedAttribute(att_type); + if (att != nullptr) { + num_att_entries += att->size(); + } + } + return num_att_entries; +} + +int SceneUtils::NumAttEntriesOnInstancedMeshes( + const Scene &scene, GeometryAttribute::Type att_type) { + const auto instances = ComputeAllInstances(scene); + int num_att_entries = 0; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + const PointAttribute *att = mesh.GetNamedAttribute(att_type); + if (att != nullptr) { + num_att_entries += att->size(); + } + } + return num_att_entries; +} + +BoundingBox SceneUtils::ComputeBoundingBox(const Scene &scene) { + // Compute bounding box that includes all scene mesh instances. + const auto instances = ComputeAllInstances(scene); + BoundingBox scene_bbox; + for (MeshInstanceIndex i(0); i < instances.size(); i++) { + const MeshInstance &instance = instances[i]; + const BoundingBox mesh_bbox = + ComputeMeshInstanceBoundingBox(scene, instance); + scene_bbox.Update(mesh_bbox); + } + return scene_bbox; +} + +BoundingBox SceneUtils::ComputeMeshInstanceBoundingBox( + const Scene &scene, const MeshInstance &instance) { + const Mesh &mesh = scene.GetMesh(instance.mesh_index); + BoundingBox mesh_bbox; + auto pc_att = mesh.GetNamedAttribute(GeometryAttribute::POSITION); + Eigen::Vector4d position; + position[3] = 1.0; + for (AttributeValueIndex i(0); i < pc_att->size(); ++i) { + pc_att->ConvertValue(i, &position[0]); + const Eigen::Vector4d transformed = instance.transform * position; + mesh_bbox.Update({static_cast(transformed[0]), + static_cast(transformed[1]), + static_cast(transformed[2])}); + } + return mesh_bbox; +} + +namespace { + +// Updates texture pointers in mesh features of |mesh| to texture pointers +// stored in |new_texture_library|. |texture_to_index_map| stores texture +// indices of the old texture pointers within |mesh|. +void UpdateMeshFeaturesTexturesOnMesh( + const std::unordered_map &texture_to_index_map, + TextureLibrary *new_texture_library, Mesh *mesh) { + for (MeshFeaturesIndex mfi(0); mfi < mesh->NumMeshFeatures(); ++mfi) { + mesh->UpdateMeshFeaturesTexturePointer( + texture_to_index_map, new_texture_library, &mesh->GetMeshFeatures(mfi)); + } +} + +} // namespace + +StatusOr> SceneUtils::MeshToScene( + std::unique_ptr mesh) { + const size_t num_mesh_materials = mesh->GetMaterialLibrary().NumMaterials(); + std::unique_ptr scene(new Scene()); + if (num_mesh_materials > 0) { + scene->GetMaterialLibrary().Copy(mesh->GetMaterialLibrary()); + mesh->GetMaterialLibrary().Clear(); + } else { + // Create a default material for the scene. + scene->GetMaterialLibrary().MutableMaterial(0); + } + + // Copy mesh feature textures. + scene->GetNonMaterialTextureLibrary().Copy( + mesh->GetNonMaterialTextureLibrary()); + + const auto old_texture_to_index_map = + mesh->GetNonMaterialTextureLibrary().ComputeTextureToIndexMap(); + + const SceneNodeIndex scene_node_index = scene->AddNode(); + SceneNode *const scene_node = scene->GetNode(scene_node_index); + const MeshGroupIndex mesh_group_index = scene->AddMeshGroup(); + MeshGroup *const mesh_group = scene->GetMeshGroup(mesh_group_index); + + if (num_mesh_materials <= 1) { + const MeshIndex mesh_index = scene->AddMesh(std::move(mesh)); + if (mesh_index == kInvalidMeshIndex) { + // No idea whether this can happen. It's not covered by any unit test. + return Status(Status::DRACO_ERROR, "Could not add Draco mesh to scene."); + } + mesh_group->AddMeshInstance({mesh_index, 0, {}}); + + UpdateMeshFeaturesTexturesOnMesh(old_texture_to_index_map, + &scene->GetNonMaterialTextureLibrary(), + &scene->GetMesh(mesh_index)); + + } else { + const int32_t mat_att_id = + mesh->GetNamedAttributeId(GeometryAttribute::MATERIAL); + if (mat_att_id == -1) { + // Probably dead code, not covered by any unit test. + return Status(Status::DRACO_ERROR, + "Internal error in MeshToScene: " + "GetNamedAttributeId(MATERIAL) returned -1"); + } + const PointAttribute *const mat_att = + mesh->GetNamedAttribute(GeometryAttribute::MATERIAL); + if (mat_att == nullptr) { + // Probably dead code, not covered by any unit test. + return Status(Status::DRACO_ERROR, + "Internal error in MeshToScene: " + "GetNamedAttribute(MATERIAL) returned nullptr"); + } + + MeshSplitter splitter; + DRACO_ASSIGN_OR_RETURN(MeshSplitter::MeshVector split_meshes, + splitter.SplitMesh(*mesh, mat_att_id)); + // Note: cannot clear mesh here, since mat_att points into it. + for (size_t i = 0; i < split_meshes.size(); ++i) { + if (split_meshes[i] == nullptr) { + // Probably dead code, not covered by any unit test. + continue; + } + const MeshIndex mesh_index = scene->AddMesh(std::move(split_meshes[i])); + if (mesh_index == kInvalidMeshIndex) { + // No idea whether this can happen. It's not covered by any unit test. + return Status(Status::DRACO_ERROR, + "Could not add Draco mesh to scene."); + } + + int material_index = 0; + mat_att->GetValue(AttributeValueIndex(i), &material_index); + mesh_group->AddMeshInstance({mesh_index, material_index, {}}); + + // Copy over mesh features that were associated with the |material_index|. + Mesh &scene_mesh = scene->GetMesh(mesh_index); + Mesh::CopyMeshFeaturesForMaterial(*mesh, &scene_mesh, material_index); + UpdateMeshFeaturesTexturesOnMesh(old_texture_to_index_map, + &scene->GetNonMaterialTextureLibrary(), + &scene_mesh); + } + } + + scene_node->SetMeshGroupIndex(mesh_group_index); + scene->AddRootNodeIndex(scene_node_index); + return scene; +} + +void SceneUtils::PrintInfo(const Scene &input, const Scene &simplified, + bool verbose) { + struct Printer { + Printer(const Scene &input, const Scene &simplified) + : input(input), simplified(simplified), print_instanced_info(false) { + // Info about the instanced meshes is printed if some of the meshes have + // multiple instances and also if the number of base meshes has changed. + auto input_instances = SceneUtils::NumMeshInstances(input); + auto simplified_instances = SceneUtils::NumMeshInstances(simplified); + if (input_instances.size() != simplified_instances.size()) { + print_instanced_info = true; + return; + } + for (MeshIndex i(0); i < input_instances.size(); i++) { + if (input_instances[i] != 1 || simplified_instances[i] != 1) { + print_instanced_info = true; + return; + } + } + } + + void PrintInfoHeader() const { + printf("\n"); + printf("%21s | geometry: base", ""); + if (print_instanced_info) { + printf(" instanced"); + } + printf("\n"); + } + + void PrintInfoRow(const std::string &label, int count_input_base, + int count_input_instanced, int count_simplified_base, + int count_simplified_instanced) const { + // Do not clutter the printout with empty info. + if (count_input_base == 0 && count_input_instanced == 0) { + return; + } + printf(" ----------------------------------------------"); + if (print_instanced_info) { + printf("-------------"); + } + printf("\n"); + printf("%21s | input: %12d", label.c_str(), count_input_base); + if (print_instanced_info) { + printf(" %12d", count_input_instanced); + } + printf("\n"); + printf("%21s | simplified: %12d", "", count_simplified_base); + if (print_instanced_info) { + printf(" %12d", count_simplified_instanced); + } + printf("\n"); + } + + void PrintAttInfoRow(const std::string &label, const draco::Scene &input, + const draco::Scene &simplified, + draco::GeometryAttribute::Type att_type) const { + PrintInfoRow(label, NumAttEntriesOnBaseMeshes(input, att_type), + NumAttEntriesOnInstancedMeshes(input, att_type), + NumAttEntriesOnBaseMeshes(simplified, att_type), + NumAttEntriesOnInstancedMeshes(simplified, att_type)); + } + + const Scene &input; + const Scene &simplified; + bool print_instanced_info; + }; + + // Print information about input and simplified scenes. + const Printer printer(input, simplified); + printer.PrintInfoHeader(); + if (verbose) { + const int num_meshes_input_base = input.NumMeshes(); + const int num_meshes_simplified_base = simplified.NumMeshes(); + const int num_meshes_input_instanced = ComputeAllInstances(input).size(); + const int num_meshes_simplified_instanced = + ComputeAllInstances(simplified).size(); + printer.PrintInfoRow("Number of meshes", num_meshes_input_base, + num_meshes_input_instanced, num_meshes_simplified_base, + num_meshes_simplified_instanced); + } + printer.PrintInfoRow("Number of faces", NumFacesOnBaseMeshes(input), + NumFacesOnInstancedMeshes(input), + NumFacesOnBaseMeshes(simplified), + NumFacesOnInstancedMeshes(simplified)); + if (verbose) { + printer.PrintInfoRow("Number of points", NumPointsOnBaseMeshes(input), + NumPointsOnInstancedMeshes(input), + NumPointsOnBaseMeshes(simplified), + NumPointsOnInstancedMeshes(simplified)); + printer.PrintAttInfoRow("Number of positions", input, simplified, + draco::GeometryAttribute::POSITION); + printer.PrintAttInfoRow("Number of normals", input, simplified, + draco::GeometryAttribute::NORMAL); + printer.PrintAttInfoRow("Number of colors", input, simplified, + draco::GeometryAttribute::COLOR); + printer.PrintInfoRow("Number of materials", + input.GetMaterialLibrary().NumMaterials(), + simplified.GetMaterialLibrary().NumMaterials(), + input.GetMaterialLibrary().NumMaterials(), + simplified.GetMaterialLibrary().NumMaterials()); + } +} + +StatusOr> SceneUtils::InstantiateMesh( + const Scene &scene, const MeshInstance &instance) { + // Check if the |scene| has base mesh corresponding to mesh |instance|. + if (scene.NumMeshes() <= instance.mesh_index.value()) { + Status(Status::DRACO_ERROR, "Scene has no corresponding base mesh."); + } + + // Check that mesh has valid positions. + const Mesh &base_mesh = scene.GetMesh(instance.mesh_index); + const int32_t pos_id = + base_mesh.GetNamedAttributeId(GeometryAttribute::POSITION); + const PointAttribute *const pos_att = base_mesh.attribute(pos_id); + if (pos_att == nullptr) { + return Status(Status::DRACO_ERROR, "Mesh has no positions."); + } + if (pos_att->data_type() != DT_FLOAT32 || pos_att->num_components() != 3) { + return Status(Status::DRACO_ERROR, "Mesh has invalid positions."); + } + + // Copy the base mesh from |scene|. + std::unique_ptr mesh(new Mesh()); + mesh->Copy(base_mesh); + + // Apply transformation to mesh unless transformation is identity. + if (instance.transform != Eigen::Matrix4d::Identity()) { + MeshUtils::TransformMesh(instance.transform, mesh.get()); + } + return mesh; +} + +namespace { + +// Helper class for deleting unused nodes from the scene. +class SceneUnusedNodeRemover { + public: + // Removes unused nodes from the |scene|. + void RemoveUnusedNodes(Scene *scene) { + // Finds all unused nodes and initializes |node_map_| that maps old node + // indices to new node indices. + const int num_unused_nodes = FindUnusedNodes(*scene); + if (num_unused_nodes == 0) { + return; // All nodes are used. + } + + // Update indices of all scene elements accounting for nodes that are going + // to be removed from the scene. + UpdateNodeIndices(scene); + RemoveUnusedNodesFromScene(scene); + } + + private: + // Returns the number of unused nodes. + int FindUnusedNodes(const Scene &scene) { + // First all nodes are considered unused (mapped to invalid index). + // Initially if a node is used, we just map it to its own index. The final + // mapping will be updated once we know all used nodes. + node_map_.resize(scene.NumNodes(), kInvalidSceneNodeIndex); + for (SceneNodeIndex sni(0); sni < scene.NumNodes(); ++sni) { + // If the scene node has a valid mesh group, mark it as used. + if (scene.GetNode(sni)->GetMeshGroupIndex() != kInvalidMeshGroupIndex) { + node_map_[sni] = sni; + } + } + + // Preserve nodes used by animations. + for (AnimationIndex i(0); i < scene.NumAnimations(); i++) { + const Animation &animation = *scene.GetAnimation(i); + for (int channel_i = 0; channel_i < animation.NumChannels(); + channel_i++) { + const SceneNodeIndex node_index( + animation.GetChannel(channel_i)->target_index); + node_map_[node_index] = node_index; + } + } + for (SkinIndex i(0); i < scene.NumSkins(); i++) { + const Skin &skin = *scene.GetSkin(i); + for (int j = 0; j < skin.NumJoints(); j++) { + const SceneNodeIndex node_index = skin.GetJoint(j); + node_map_[node_index] = node_index; + } + const SceneNodeIndex root_index = skin.GetJointRoot(); + if (root_index != kInvalidSceneNodeIndex) { + node_map_[root_index] = root_index; + } + } + + // Ensure that "unused" nodes with used child nodes are marked as used + // (a node can't be deleted as long as it has a used child node). + for (int r = 0; r < scene.NumRootNodes(); ++r) { + UpdateUsedNodesFromSceneGraph(scene, scene.GetRootNodeIndex(r)); + } + + // All used / unused nodes are known. Find new indices for all scene nodes. + int num_valid_nodes = 0; + for (SceneNodeIndex sni(0); sni < scene.NumNodes(); ++sni) { + if (node_map_[sni] != kInvalidSceneNodeIndex) { + node_map_[sni] = SceneNodeIndex(num_valid_nodes++); + } + } + // Return the number of nodes that were unused. + return scene.NumNodes() - num_valid_nodes; + } + + // Recursively traverse node |sni| and mark it as used as long as it has a + // used child node. The function returns true when |sni| is a used node. + bool UpdateUsedNodesFromSceneGraph(const Scene &scene, SceneNodeIndex sni) { + const auto &node = scene.GetNode(sni); + bool is_any_child_node_used = false; + for (int c = 0; c < node->NumChildren(); ++c) { + const SceneNodeIndex cni = node->Child(c); + // Check if the child node is used. + const bool is_c_used = UpdateUsedNodesFromSceneGraph(scene, cni); + if (is_c_used) { + is_any_child_node_used = true; + } + } + if (is_any_child_node_used) { + // The node must be used even if it was previously marked as unused. + node_map_[sni] = sni; + } + // Returns whether this node is used or not. + return node_map_[sni] != kInvalidSceneNodeIndex; + } + + // Remaps existing node indices at various scene elements to new node indices + // defined by |node_map_|. + void UpdateNodeIndices(Scene *scene) const { + // Update node indices on child / parent nodes. + std::vector indices; + for (SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + indices = scene->GetNode(sni)->Children(); + scene->GetNode(sni)->RemoveAllChildren(); + for (int j = 0; j < indices.size(); ++j) { + const SceneNodeIndex new_sni = node_map_[indices[j]]; + if (new_sni != kInvalidSceneNodeIndex) { + scene->GetNode(sni)->AddChildIndex(new_sni); + } + } + indices = scene->GetNode(sni)->Parents(); + scene->GetNode(sni)->RemoveAllParents(); + for (int j = 0; j < indices.size(); ++j) { + const SceneNodeIndex new_sni = node_map_[indices[j]]; + if (new_sni != kInvalidSceneNodeIndex) { + scene->GetNode(sni)->AddParentIndex(new_sni); + } + } + } + + // Update root node indices. + indices = scene->GetRootNodeIndices(); + scene->RemoveAllRootNodeIndices(); + for (int ri = 0; ri < indices.size(); ++ri) { + const SceneNodeIndex new_rni = node_map_[indices[ri]]; + if (new_rni != kInvalidSceneNodeIndex) { + scene->AddRootNodeIndex(new_rni); + } + } + + // Update node indices used by animations. + for (AnimationIndex i(0); i < scene->NumAnimations(); i++) { + Animation &animation = *scene->GetAnimation(i); + for (int i = 0; i < animation.NumChannels(); i++) { + const SceneNodeIndex node_index(animation.GetChannel(i)->target_index); + animation.GetChannel(i)->target_index = node_map_[node_index].value(); + } + } + for (SkinIndex i(0); i < scene->NumSkins(); i++) { + Skin &skin = *scene->GetSkin(i); + for (int j = 0; j < skin.NumJoints(); j++) { + const SceneNodeIndex node_index = skin.GetJoint(j); + skin.GetJoint(j) = node_map_[node_index]; + } + const SceneNodeIndex root_index = skin.GetJointRoot(); + if (root_index != kInvalidSceneNodeIndex) { + skin.SetJointRoot(node_map_[root_index]); + } + } + } + + // Removes all unused nodes from the scene. + void RemoveUnusedNodesFromScene(Scene *scene) const { + int num_valid_nodes = 0; + // Copy over nodes to their new position in the nodes array. + for (SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + const SceneNodeIndex new_sni = node_map_[sni]; + if (new_sni == kInvalidSceneNodeIndex) { + continue; + } + num_valid_nodes++; + if (sni != new_sni) { + // Copy over the |sni| node to the new location (|new_sni| is lower than + // |sni|). + scene->GetNode(new_sni)->Copy(*scene->GetNode(sni)); + } + } + // Resize the nodes in the scene to account for the unused ones. This will + // delete all unused nodes. + scene->ResizeNodes(num_valid_nodes); + } + + IndexTypeVector node_map_; +}; + +} // namespace + +void SceneUtils::Cleanup(Scene *scene) { Cleanup(scene, CleanupOptions()); } + +void SceneUtils::Cleanup(Scene *scene, const CleanupOptions &options) { + // Remove invalid mesh indices from mesh groups. + if (options.remove_invalid_mesh_instances) { + for (MeshGroupIndex i(0); i < scene->NumMeshGroups(); i++) { + scene->GetMeshGroup(i)->RemoveMeshInstances(kInvalidMeshIndex); + } + } + + // Find references to mesh groups. + std::vector is_mesh_group_referenced(scene->NumMeshGroups(), false); + for (SceneNodeIndex i(0); i < scene->NumNodes(); i++) { + const SceneNode &node = *scene->GetNode(i); + const MeshGroupIndex mesh_group_index = node.GetMeshGroupIndex(); + if (mesh_group_index != kInvalidMeshGroupIndex) { + is_mesh_group_referenced[mesh_group_index.value()] = true; + } + } + + // Find references to base meshes from referenced mesh groups and find mesh + // groups that have no valid references to base meshes. + std::vector is_base_mesh_referenced(scene->NumMeshes(), false); + std::vector is_mesh_group_empty(scene->NumMeshGroups(), false); + for (MeshGroupIndex i(0); i < scene->NumMeshGroups(); i++) { + if (!is_mesh_group_referenced[i.value()]) { + continue; + } + const MeshGroup &mesh_group = *scene->GetMeshGroup(i); + bool mesh_group_is_empty = true; + for (int j = 0; j < mesh_group.NumMeshInstances(); j++) { + const MeshIndex mesh_index = mesh_group.GetMeshInstance(j).mesh_index; + mesh_group_is_empty = false; + is_base_mesh_referenced[mesh_index.value()] = true; + } + if (mesh_group_is_empty) { + is_mesh_group_empty[i.value()] = true; + } + } + + if (options.remove_unused_meshes) { + // Remove base meshes with no references to them. + for (int i = scene->NumMeshes() - 1; i >= 0; i--) { + const MeshIndex mi(i); + if (!is_base_mesh_referenced[mi.value()]) { + scene->RemoveMesh(mi); + } + } + } + + if (options.remove_unused_mesh_groups) { + // Remove empty mesh groups with no geometry or no references to them. + for (int i = scene->NumMeshGroups() - 1; i >= 0; i--) { + const MeshGroupIndex mgi(i); + if (is_mesh_group_empty[mgi.value()] || + !is_mesh_group_referenced[mgi.value()]) { + scene->RemoveMeshGroup(mgi); + } + } + } + + // Find materials that reference a texture. + MaterialLibrary &material_library = scene->GetMaterialLibrary(); + std::vector materials_with_textures(material_library.NumMaterials(), + false); + for (int i = 0; i < material_library.NumMaterials(); ++i) { + if (material_library.GetMaterial(i)->NumTextureMaps() > 0) { + materials_with_textures[i] = true; + } + } + + // Maps material index to a set of meshes that use that material. + std::vector> material_meshes( + material_library.NumMaterials()); + + // Maps mesh index to a set of materials used by that mesh. + IndexTypeVector> mesh_materials( + scene->NumMeshes()); + + // Maps mesh index to a set of tex coord indices referenced by materials. + IndexTypeVector> tex_coord_referenced( + scene->NumMeshes()); + + // Populate the maps that will be used to remove unused texture coordinates. + for (int mgi = 0; mgi < scene->NumMeshGroups(); ++mgi) { + const MeshGroup *const mesh_group = + scene->GetMeshGroup(MeshGroupIndex(mgi)); + for (int mi = 0; mi < mesh_group->NumMeshInstances(); ++mi) { + const MeshIndex mesh_index = mesh_group->GetMeshInstance(mi).mesh_index; + const int material_index = mesh_group->GetMeshInstance(mi).material_index; + if (material_index == -1) { + continue; + } + + // Populate mesh-material mapping. + material_meshes[material_index].insert(mesh_index); + mesh_materials[mesh_index].insert(material_index); + + // Populate texture coordinate indices referenced by material textures. + const auto material = material_library.GetMaterial(material_index); + for (int i = 0; i < material->NumTextureMaps(); i++) { + const TextureMap *const texture_map = material->GetTextureMapByIndex(i); + const int tex_coord_index = texture_map->tex_coord_index(); + tex_coord_referenced[mesh_index].insert(tex_coord_index); + } + } + } + + // From each mesh, remove texture coordinate attributes that are not + // referenced by any materials and decrement texture coordinate indices in + // texture maps of the mesh materials accordingly. + if (options.remove_unused_tex_coords) { + for (MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + // Do not remove unreferenced texture coordinates when the mesh materials + // are used by any other meshes to avoid corrupting those other meshes. + // TODO(vytyaz): Consider removing this limitation. + bool remove_tex_coord = true; + for (const int material_index : mesh_materials[mi]) { + if (material_meshes[material_index].size() != 1) { + // Materials of this mesh are used by other meshes. + remove_tex_coord = false; + break; + } + } + if (!remove_tex_coord) { + continue; + } + + // Remove unreferenced texture coordinate sets from this mesh. + Mesh &mesh = scene->GetMesh(mi); + const int tex_coord_count = + mesh.NumNamedAttributes(GeometryAttribute::TEX_COORD); + for (int tci = tex_coord_count - 1; tci >= 0; tci--) { + if (tex_coord_referenced[mi].count(tci) != 0) { + // Texture coordinate set is referenced. + continue; + } + mesh.DeleteAttribute( + mesh.GetNamedAttributeId(GeometryAttribute::TEX_COORD, tci)); + + // Decrement texture coordinate indices in all materials of this mesh. + for (const int material_index : mesh_materials[mi]) { + auto material = material_library.MutableMaterial(material_index); + for (int i = 0; i < material->NumTextureMaps(); i++) { + auto texture_map = material->GetTextureMapByIndex(i); + // Decrement the indices that are greater than the removed index. + if (texture_map->tex_coord_index() > tci) { + texture_map->SetProperties(texture_map->type(), + texture_map->tex_coord_index() - 1); + } + } + } + } + } + } + + if (options.remove_unused_materials) { + // Remove materials that are not used by any mesh. + for (int i = material_library.NumMaterials() - 1; i >= 0; --i) { + if (material_meshes[i].empty()) { + // Material |i| is not used. + scene->RemoveMaterial(i); + } + } + } + + if (options.remove_unused_nodes) { + SceneUnusedNodeRemover node_remover; + node_remover.RemoveUnusedNodes(scene); + } +} + +void SceneUtils::RemoveMeshInstances(const std::vector &instances, + Scene *scene) { + // Remove mesh instances from the scene. + for (const SceneUtils::MeshInstance &instance : instances) { + const MeshGroupIndex mgi = + scene->GetNode(instance.scene_node_index)->GetMeshGroupIndex(); + + // Create a new mesh group with removed instance (we can't just delete the + // instance from the mesh group directly, because the same mesh group may + // be used by multiple scene nodes). + const MeshGroupIndex new_mesh_group_index = scene->AddMeshGroup(); + MeshGroup &new_mesh_group = *scene->GetMeshGroup(new_mesh_group_index); + + new_mesh_group.Copy(*scene->GetMeshGroup(mgi)); + new_mesh_group.RemoveMeshInstances(instance.mesh_index); + + // Assign the new mesh group to the scene node. Unused mesh groups will be + // automatically removed later during a scene cleanup operation. + scene->GetNode(instance.scene_node_index) + ->SetMeshGroupIndex(new_mesh_group_index); + } + + // Remove duplicate mesh groups that may have been created during the instance + // removal process. + DeduplicateMeshGroups(scene); +} + +void SceneUtils::DeduplicateMeshGroups(Scene *scene) { + if (scene->NumMeshGroups() <= 1) { + return; + } + + // Signature of a mesh group used for detecting duplicates. + struct MeshGroupSignature { + const MeshGroupIndex mesh_group_index; + const MeshGroup &mesh_group; + MeshGroupSignature(MeshGroupIndex mgi, const MeshGroup &mesh_group) + : mesh_group_index(mgi), mesh_group(mesh_group) {} + + bool operator==(const MeshGroupSignature &signature) const { + if (mesh_group.GetName() != signature.mesh_group.GetName()) { + return false; + } + if (mesh_group.NumMeshInstances() != + signature.mesh_group.NumMeshInstances()) { + return false; + } + // TODO(ostava): We may consider sorting meshes within a mesh group to + // make the order of meshes irrelevant. This should be done only for + // meshes with opaque materials though, because for transparent + // geometries, the order matters. + for (int i = 0; i < mesh_group.NumMeshInstances(); ++i) { + if (mesh_group.GetMeshInstance(i) != + signature.mesh_group.GetMeshInstance(i)) { + return false; + } + } + return true; + } + struct Hash { + size_t operator()(const MeshGroupSignature &signature) const { + size_t hash = 79; // Magic number. + const MeshGroup &group = signature.mesh_group; + hash = HashCombine(group.GetName(), hash); + hash = HashCombine(group.NumMeshInstances(), hash); + for (int i = 0; i < group.NumMeshInstances(); ++i) { + const MeshGroup::MeshInstance &instance = group.GetMeshInstance(i); + hash = HashCombine(instance.mesh_index, hash); + hash = HashCombine(instance.material_index, hash); + hash = HashCombine(instance.materials_variants_mappings.size(), hash); + for (const MeshGroup::MaterialsVariantsMapping &mapping : + instance.materials_variants_mappings) { + hash = HashCombine(mapping.material, hash); + hash = HashCombine(mapping.variants.size(), hash); + for (const int &variant : mapping.variants) { + hash = HashCombine(variant, hash); + } + } + } + return hash; + } + }; + }; + + // Set holding unique mesh groups. + std::unordered_set + unique_mesh_groups; + IndexTypeVector parent_mesh_group( + scene->NumMeshGroups()); + for (MeshGroupIndex mgi(0); mgi < scene->NumMeshGroups(); ++mgi) { + const MeshGroup *mg = scene->GetMeshGroup(mgi); + const MeshGroupSignature signature(mgi, *mg); + auto it = unique_mesh_groups.find(signature); + if (it != unique_mesh_groups.end()) { + parent_mesh_group[mgi] = it->mesh_group_index; + } else { + parent_mesh_group[mgi] = kInvalidMeshGroupIndex; + unique_mesh_groups.insert(signature); + } + } + + // Go over all nodes and update mesh groups if needed. + for (SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + const MeshGroupIndex mgi = scene->GetNode(sni)->GetMeshGroupIndex(); + if (mgi == kInvalidMeshGroupIndex || + parent_mesh_group[mgi] == kInvalidMeshGroupIndex) { + continue; // Nothing to update. + } + scene->GetNode(sni)->SetMeshGroupIndex(parent_mesh_group[mgi]); + } + + // Remove any unused mesh groups. + Cleanup(scene); +} + +void SceneUtils::SetDracoCompressionOptions( + const DracoCompressionOptions *options, Scene *scene) { + for (MeshIndex i(0); i < scene->NumMeshes(); ++i) { + Mesh &mesh = scene->GetMesh(i); + if (options == nullptr) { + mesh.SetCompressionEnabled(false); + } else { + mesh.SetCompressionEnabled(true); + mesh.SetCompressionOptions(*options); + } + } +} + +bool SceneUtils::IsDracoCompressionEnabled(const Scene &scene) { + for (MeshIndex i(0); i < scene.NumMeshes(); ++i) { + if (scene.GetMesh(i).IsCompressionEnabled()) { + return true; + } + } + return false; +} + +IndexTypeVector +SceneUtils::FindLargestBaseMeshTransforms(const Scene &scene) { + IndexTypeVector transforms( + scene.NumMeshes(), Eigen::Matrix4d::Identity()); + + // In case a mesh has multiple instances we want to use the instance with + // the largest scale. + IndexTypeVector transform_scale(scene.NumMeshes(), 0.f); + + const auto instances = SceneUtils::ComputeAllInstances(scene); + for (MeshInstanceIndex i(0); i < instances.size(); ++i) { + const auto &instance = instances[i]; + + // Compute the scale of the transform. + const Vector3f scale_vec(instance.transform.col(0).norm(), + instance.transform.col(1).norm(), + instance.transform.col(2).norm()); + + // In our framework we support uniform scale only. For now, just take the + // maximum scale across all axes. + // TODO(ostava): Investigate how to properly support non-uniform scaling. + const float max_scale = scale_vec.MaxCoeff(); + + if (transform_scale[instance.mesh_index] < max_scale) { + transform_scale[instance.mesh_index] = max_scale; + transforms[instance.mesh_index] = instance.transform; + } + } + + return transforms; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/scene_utils.h b/contrib/draco/src/draco/scene/scene_utils.h new file mode 100644 index 000000000..5b978c3c5 --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_utils.h @@ -0,0 +1,150 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_SCENE_SCENE_UTILS_H_ +#define DRACO_SCENE_SCENE_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/attributes/geometry_attribute.h" +#include "draco/scene/scene.h" + +namespace draco { + +// Helper class containing various utility functions operating on draco::Scene. +class SceneUtils { + public: + // Helper struct holding instanced meshes and their transformations. + struct MeshInstance { + // Index of the parent mesh in the draco::Scene. + MeshIndex mesh_index; + // Index of the node in the draco::Scene. + SceneNodeIndex scene_node_index; + // Index of the mesh in the mesh group. + int mesh_group_mesh_index; + // Transform of the instance from the mesh local space to the global space + // of the scene. + Eigen::Matrix4d transform; + }; + + // Computes all mesh instances in the |scene|. + static IndexTypeVector ComputeAllInstances( + const Scene &scene); + + // Computes global transform matrix of a |scene| node given by its |index|. + static Eigen::Matrix4d ComputeGlobalNodeTransform(const Scene &scene, + SceneNodeIndex index); + + // Returns a vector of mesh instance counts for all base meshes. + static IndexTypeVector NumMeshInstances(const Scene &scene); + + // Returns the material index of the given |instance| or -1 if the mesh + // |instance| has a default material. + static int GetMeshInstanceMaterialIndex(const Scene &scene, + const MeshInstance &instance); + + // Returns the total number of faces on all base meshes of the scene (not + // counting instances). + static int NumFacesOnBaseMeshes(const Scene &scene); + + // Returns the total number of faces on all meshes of the scenes, including + // all instances of the same mesh. + static int NumFacesOnInstancedMeshes(const Scene &scene); + + // Returns the total number of points on all base meshes of the scene (not + // counting instances). + static int NumPointsOnBaseMeshes(const Scene &scene); + + // Returns the total number of points on all meshes of the scenes, including + // all instances of the same mesh. + static int NumPointsOnInstancedMeshes(const Scene &scene); + + // Returns the total number of attribute entries on all base meshes of the + // scene (not counting instances) for the first attribute of |att_type|. + static int NumAttEntriesOnBaseMeshes(const Scene &scene, + GeometryAttribute::Type att_type); + + // Returns the total number of attribute ent on all meshes of the scenes, + // including all instances of the same mesh for the first attribute of + // |att_type|. + static int NumAttEntriesOnInstancedMeshes(const Scene &scene, + GeometryAttribute::Type att_type); + + // Returns the bounding box of the scene. + static BoundingBox ComputeBoundingBox(const Scene &scene); + + // Returns the bounding box of a mesh instance. + static BoundingBox ComputeMeshInstanceBoundingBox( + const Scene &scene, const MeshInstance &instance); + + // Prints info about input and simplified scenes. + static void PrintInfo(const Scene &input, const Scene &simplified, + bool verbose); + + // Converts a draco::Mesh into a draco::Scene. If the passed-in `mesh` has + // multiple materials, the returned scene will contain multiple meshes, one + // for each of the source mesh's materials; if `mesh` has no material, one + // will be created for it. + static StatusOr> MeshToScene( + std::unique_ptr mesh); + + // Creates a mesh according to mesh |instance| in |scene|. Error is returned + // if there is no corresponding base mesh in the |scene| or the base mesh has + // no valid positions. + static StatusOr> InstantiateMesh( + const Scene &scene, const MeshInstance &instance); + + // Cleans up a |scene| by removing unused base meshes, unused and empty mesh + // groups, unused materials, unused texture coordinates and unused scene + // nodes. The actual behavior of the cleanup operation can be controller via + // the user provided |options|. + struct CleanupOptions { + bool remove_invalid_mesh_instances = true; + bool remove_unused_mesh_groups = true; + bool remove_unused_meshes = true; + bool remove_unused_nodes = false; + bool remove_unused_tex_coords = false; + bool remove_unused_materials = true; + }; + static void Cleanup(Scene *scene); + static void Cleanup(Scene *scene, const CleanupOptions &options); + + // Removes mesh |instances| from |scene|. + static void RemoveMeshInstances(const std::vector &instances, + Scene *scene); + + // Removes duplicate mesh groups that have the same name and that contain + // exactly the same meshes and materials. + static void DeduplicateMeshGroups(Scene *scene); + + // Enables geometry compression and sets compression |options| to all meshes + // in the |scene|. If |options| is nullptr then geometry compression is + // disabled for all meshes in the |scene|. + static void SetDracoCompressionOptions(const DracoCompressionOptions *options, + Scene *scene); + + // Returns true if geometry compression is eabled for any of |scene| meshes. + static bool IsDracoCompressionEnabled(const Scene &scene); + + // Returns a single tranformation matrix for each base mesh of the |scene| + // corresponding to the instance with the maximum scale. + static IndexTypeVector + FindLargestBaseMeshTransforms(const Scene &scene); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_SCENE_UTILS_H_ diff --git a/contrib/draco/src/draco/scene/scene_utils_test.cc b/contrib/draco/src/draco/scene/scene_utils_test.cc new file mode 100644 index 000000000..4d6bd731d --- /dev/null +++ b/contrib/draco/src/draco/scene/scene_utils_test.cc @@ -0,0 +1,763 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/scene_utils.h" + +#include +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/bounding_box.h" +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" +#include "draco/scene/scene_indices.h" + +namespace { + +using draco::MeshIndex; +using draco::MeshInstanceIndex; + +void AssertMatrixNear(const Eigen::Matrix4d &a, const Eigen::Matrix4d &b, + float tolerance) { + Eigen::Matrix4d diff = a - b; + ASSERT_NEAR(diff.norm(), 0.f, tolerance) << a << " vs " << b; +} + +// TODO(fgalligan): Re-factor this code with gltf_encoder_test. +void CompareScenes(const draco::Scene *scene0, const draco::Scene *scene1) { + ASSERT_EQ(scene0->NumMeshGroups(), scene1->NumMeshGroups()); + ASSERT_EQ(scene0->NumMeshes(), scene1->NumMeshes()); + ASSERT_EQ(scene0->GetMaterialLibrary().NumMaterials(), + scene1->GetMaterialLibrary().NumMaterials()); + ASSERT_EQ(scene0->NumAnimations(), scene1->NumAnimations()); + ASSERT_EQ(scene0->NumSkins(), scene1->NumSkins()); + for (draco::AnimationIndex i(0); i < scene0->NumAnimations(); ++i) { + const draco::Animation *const animation0 = scene0->GetAnimation(i); + const draco::Animation *const animation1 = scene1->GetAnimation(i); + ASSERT_NE(animation0, nullptr); + ASSERT_NE(animation1, nullptr); + ASSERT_EQ(animation0->NumSamplers(), animation1->NumSamplers()); + ASSERT_EQ(animation0->NumChannels(), animation1->NumChannels()); + ASSERT_EQ(animation0->NumNodeAnimationData(), + animation1->NumNodeAnimationData()); + } +} + +TEST(SceneUtilsTest, TestComputeAllInstances) { + // Tests that we can compute all instances in an input scene along with their + // transformations. + + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + // Compute mesh instances. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 5); + + // Check base mesh indices. + ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(1)].mesh_index, 1); + ASSERT_EQ(instances[MeshInstanceIndex(2)].mesh_index, 2); + ASSERT_EQ(instances[MeshInstanceIndex(3)].mesh_index, 3); + ASSERT_EQ(instances[MeshInstanceIndex(4)].mesh_index, 3); + + // Check scene node indices. + ASSERT_EQ(instances[MeshInstanceIndex(0)].scene_node_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(1)].scene_node_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(2)].scene_node_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(3)].scene_node_index, 4); + ASSERT_EQ(instances[MeshInstanceIndex(4)].scene_node_index, 2); + + // Check indices of meshes in mesh group. + ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_group_mesh_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(1)].mesh_group_mesh_index, 1); + ASSERT_EQ(instances[MeshInstanceIndex(2)].mesh_group_mesh_index, 2); + ASSERT_EQ(instances[MeshInstanceIndex(3)].mesh_group_mesh_index, 0); + ASSERT_EQ(instances[MeshInstanceIndex(4)].mesh_group_mesh_index, 0); + + // The first three instances should have identity transformation. + for (MeshInstanceIndex i(0); i < 3; ++i) { + AssertMatrixNear(instances[i].transform, Eigen::Matrix4d::Identity(), + 1e-6f); + } + + // Fourth and fifth instances are transformed. + Eigen::Matrix4d expected_transform = Eigen::Matrix4d::Identity(); + // Expected translation. + expected_transform(0, 3) = -1.352329969406128; + expected_transform(1, 3) = 0.4277220070362091; + expected_transform(2, 3) = -2.98022992950564e-8; + + // Expected rotation. + Eigen::Matrix4d expected_rotation = Eigen::Matrix4d::Identity(); + expected_rotation.block<3, 3>(0, 0) = + Eigen::Quaterniond(-0.9960774183273317, -0.0, -0.0, 0.08848590403795243) + .normalized() + .toRotationMatrix(); + expected_transform = expected_transform * expected_rotation; + + AssertMatrixNear(instances[MeshInstanceIndex(3)].transform, + expected_transform, 1e-6f); + + // Last instance differs only in the translation part in X axis. + expected_transform(0, 3) = 1.432669997215271; + + AssertMatrixNear(instances[MeshInstanceIndex(4)].transform, + expected_transform, 1e-6f); +} + +TEST(SceneUtilsTest, TestComputeAllInstancesWithShiftedGeometryRoot) { + // Tests that we can compute all instances in an input scene along with their + // transformations. This scene has light and camera nodes before the geometry + // node. + auto scene = draco::ReadSceneFromTestFile( + "SphereWithCircleTexture/sphere_with_circle_texture.gltf"); + ASSERT_NE(scene, nullptr); + + // There is one base mesh. + ASSERT_EQ(scene->NumMeshes(), 1); + + // There is a single mesh instance. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 1); + ASSERT_EQ(instances[MeshInstanceIndex(0)].mesh_index, 0); + + // There is no transformation. + AssertMatrixNear(instances[MeshInstanceIndex(0)].transform, + Eigen::Matrix4d::Identity(), 1e-6); +} + +TEST(SceneUtilsTest, TestNumMeshInstances) { + // Tests that we can compute mesh instance counts for all base meshes in an + // input scene. + + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + const auto num_mesh_instances = draco::SceneUtils::NumMeshInstances(*scene); + ASSERT_EQ(num_mesh_instances.size(), 4); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(0)], 1); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(1)], 1); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(2)], 1); + ASSERT_EQ(num_mesh_instances[draco::MeshIndex(3)], 2); +} + +TEST(SceneUtilsTest, TestNumFacesOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumFacesOnBaseMeshes(*scene), 2856); + ASSERT_EQ(draco::SceneUtils::NumFacesOnInstancedMeshes(*scene), 3624); +} + +TEST(SceneUtilsTest, TestNumPointsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumPointsOnBaseMeshes(*scene), 2978); + ASSERT_EQ(draco::SceneUtils::NumPointsOnInstancedMeshes(*scene), 3564); +} + +TEST(SceneUtilsTest, TestNumPositionsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes( + *scene, draco::GeometryAttribute::POSITION), + 1572); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes( + *scene, draco::GeometryAttribute::POSITION), + 1960); +} + +TEST(SceneUtilsTest, TestNumNormalsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes( + *scene, draco::GeometryAttribute::NORMAL), + 1252); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes( + *scene, draco::GeometryAttribute::NORMAL), + 1612); +} + +TEST(SceneUtilsTest, TestNumColorsOnScene) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnBaseMeshes( + *scene, draco::GeometryAttribute::COLOR), + 0); + ASSERT_EQ(draco::SceneUtils::NumAttEntriesOnInstancedMeshes( + *scene, draco::GeometryAttribute::COLOR), + 0); +} + +TEST(SceneUtilsTest, TestComputeBoundingBox) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + const draco::BoundingBox bbox = draco::SceneUtils::ComputeBoundingBox(*scene); + const draco::Vector3f min_point = bbox.GetMinPoint(); + const draco::Vector3f max_point = bbox.GetMaxPoint(); + constexpr float tolerance = 1e-4f; + EXPECT_NEAR(min_point[0], -2.43091, tolerance); + EXPECT_NEAR(min_point[1], +0.00145, tolerance); + EXPECT_NEAR(min_point[2], -1.39600, tolerance); + EXPECT_NEAR(max_point[0], +2.43800, tolerance); + EXPECT_NEAR(max_point[1], +2.58437, tolerance); + EXPECT_NEAR(max_point[2], +1.39600, tolerance); +} + +TEST(SceneUtilsTest, TestComputeMeshInstanceBoundingBox) { + auto scene = draco::ReadSceneFromTestFile( + "SphereWithCircleTexture/sphere_with_circle_texture.gltf"); + ASSERT_NE(scene, nullptr); + const draco::BoundingBox scene_bbox = + draco::SceneUtils::ComputeBoundingBox(*scene); + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 1); + const draco::BoundingBox mesh_bbox = + draco::SceneUtils::ComputeMeshInstanceBoundingBox( + *scene, instances[draco::MeshInstanceIndex(0)]); + ASSERT_EQ(scene_bbox.GetMinPoint(), mesh_bbox.GetMinPoint()); + ASSERT_EQ(scene_bbox.GetMaxPoint(), mesh_bbox.GetMaxPoint()); +} + +TEST(SceneUtilsTest, TestMeshToSceneZeroMaterials) { + const std::string filename = "cube_att.obj"; + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 0); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 1); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 1); +} + +TEST(SceneUtilsTest, TestMeshToSceneOneMaterial) { + const std::string filename = + "SphereWithCircleTexture/sphere_with_circle_texture.gltf"; + auto scene = draco::ReadSceneFromTestFile(filename); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMaterialLibrary().NumMaterials(), 1); + + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 1); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 1); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 1); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 1); + + CompareScenes(scene.get(), scene_from_mesh.get()); +} + +TEST(SceneUtilsTest, TestMeshToSceneMultipleMaterials) { + const std::string filename = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + auto scene = draco::ReadSceneFromTestFile(filename); + ASSERT_NE(scene, nullptr); + + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 4); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 4); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 4); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 4); + + // Unfortunately we can't CompareScenes(scene.get(), scene_from_mesh.get()), + // because scene has two mesh groups and scene_from_mesh has only one. +} + +TEST(SceneUtilsTest, TestMeshToSceneMultipleMeshFeatures) { + const std::string filename = "BoxesMeta/glTF/BoxesMeta.gltf"; + std::unique_ptr scene = draco::ReadSceneFromTestFile(filename); + ASSERT_NE(scene, nullptr); + std::unique_ptr mesh = draco::ReadMeshFromTestFile(filename); + ASSERT_NE(mesh, nullptr); + ASSERT_EQ(mesh->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(mesh->NumMeshFeatures(), 5); + + DRACO_ASSIGN_OR_ASSERT(const std::unique_ptr scene_from_mesh, + draco::SceneUtils::MeshToScene(std::move(mesh))); + ASSERT_NE(scene_from_mesh, nullptr); + ASSERT_EQ(scene_from_mesh->NumMeshes(), 2); + ASSERT_EQ(scene_from_mesh->GetMaterialLibrary().NumMaterials(), 2); + ASSERT_EQ(scene_from_mesh->NumMeshGroups(), 1); + const draco::MeshGroup *const mesh_group = + scene_from_mesh->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group->NumMeshInstances(), 2); + + // Meshes of the new scene should have the same properties as meshes loaded + // directly into |scene|. + for (draco::MeshIndex mi(0); mi < scene->NumMeshes(); ++mi) { + ASSERT_EQ(scene->GetMesh(mi).NumMeshFeatures(), + scene_from_mesh->GetMesh(mi).NumMeshFeatures()); + for (draco::MeshFeaturesIndex mfi(0); + mfi < scene->GetMesh(mi).NumMeshFeatures(); ++mfi) { + const auto &scene_mf = scene->GetMesh(mi).GetMeshFeatures(mfi); + const auto &scene_from_mesh_mf = + scene_from_mesh->GetMesh(mi).GetMeshFeatures(mfi); + ASSERT_EQ(scene_mf.GetAttributeIndex(), + scene_from_mesh_mf.GetAttributeIndex()); + ASSERT_EQ(scene_mf.GetPropertyTableIndex(), + scene_from_mesh_mf.GetPropertyTableIndex()); + ASSERT_EQ(scene_mf.GetLabel(), scene_from_mesh_mf.GetLabel()); + ASSERT_EQ(scene_mf.GetNullFeatureId(), + scene_from_mesh_mf.GetNullFeatureId()); + ASSERT_EQ(scene_mf.GetFeatureCount(), + scene_from_mesh_mf.GetFeatureCount()); + ASSERT_EQ(scene_mf.GetTextureChannels(), + scene_from_mesh_mf.GetTextureChannels()); + ASSERT_EQ(scene_mf.GetTextureMap().texture() != nullptr, + scene_from_mesh_mf.GetTextureMap().texture() != nullptr); + } + } +} + +TEST(SceneUtilsTest, TestInstantiateMeshWithIdentityTransformation) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + + // Compute scene mesh instances. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 5); + + // Check instantiation of mesh with identity transformation. + const draco::SceneUtils::MeshInstance instance = + instances[MeshInstanceIndex(0)]; + ASSERT_EQ(instance.transform, Eigen::Matrix4d::Identity()); + + // Instantiate this mesh instance. + DRACO_ASSIGN_OR_ASSERT(auto mesh, + draco::SceneUtils::InstantiateMesh(*scene, instance)); + const draco::Mesh &base_mesh = scene->GetMesh(instance.mesh_index); + + // Check that bounding box of the instanced mesh is same as box of base mesh. + const draco::BoundingBox instanced_bbox = mesh->ComputeBoundingBox(); + const draco::BoundingBox base_bbox = base_mesh.ComputeBoundingBox(); + ASSERT_EQ(instanced_bbox.GetMinPoint()[0], base_bbox.GetMinPoint()[0]); + ASSERT_EQ(instanced_bbox.GetMinPoint()[1], base_bbox.GetMinPoint()[1]); + ASSERT_EQ(instanced_bbox.GetMinPoint()[2], base_bbox.GetMinPoint()[2]); + ASSERT_EQ(instanced_bbox.GetMaxPoint()[0], base_bbox.GetMaxPoint()[0]); + ASSERT_EQ(instanced_bbox.GetMaxPoint()[1], base_bbox.GetMaxPoint()[1]); + ASSERT_EQ(instanced_bbox.GetMaxPoint()[2], base_bbox.GetMaxPoint()[2]); +} + +TEST(SceneUtilsTest, TestInstantiateMesh) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + + // Compute scene mesh instances. + const auto instances = draco::SceneUtils::ComputeAllInstances(*scene); + ASSERT_EQ(instances.size(), 5); + + // Check instantiation of mesh with identity transformation. + const draco::SceneUtils::MeshInstance instance = + instances[MeshInstanceIndex(3)]; + ASSERT_NE(instance.transform, Eigen::Matrix4d::Identity()); + + // Instantiate this mesh instance. + DRACO_ASSIGN_OR_ASSERT(auto mesh, + draco::SceneUtils::InstantiateMesh(*scene, instance)); + const draco::Mesh &base_mesh = scene->GetMesh(instance.mesh_index); + + // Check bounding box of the base mesh. + constexpr float tolerance = 1e-4f; + const draco::BoundingBox base_bbox = base_mesh.ComputeBoundingBox(); + EXPECT_NEAR(base_bbox.GetMinPoint()[0], -0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMinPoint()[1], -0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMinPoint()[2], -1.05800, tolerance); + EXPECT_NEAR(base_bbox.GetMaxPoint()[0], +0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMaxPoint()[1], +0.42780, tolerance); + EXPECT_NEAR(base_bbox.GetMaxPoint()[2], +1.05800, tolerance); + + // Check bounding box of the instanced mesh. It should differ. + const draco::BoundingBox instanced_bbox = mesh->ComputeBoundingBox(); + EXPECT_NEAR(instanced_bbox.GetMinPoint()[0], -1.77860, tolerance); + EXPECT_NEAR(instanced_bbox.GetMinPoint()[1], +0.00145, tolerance); + EXPECT_NEAR(instanced_bbox.GetMinPoint()[2], -1.05800, tolerance); + EXPECT_NEAR(instanced_bbox.GetMaxPoint()[0], -0.92606, tolerance); + EXPECT_NEAR(instanced_bbox.GetMaxPoint()[1], +0.85399, tolerance); + EXPECT_NEAR(instanced_bbox.GetMaxPoint()[2], +1.05800, tolerance); +} + +TEST(SceneUtilsTest, TestCleanupEmptyMeshGroup) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5); + ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + + // Invalidate references to the three truck body parts in mesh group. + draco::MeshGroup &mesh_group = *scene->GetMeshGroup(draco::MeshGroupIndex(0)); + mesh_group.SetMeshInstance(0, {draco::kInvalidMeshIndex, 0}); + mesh_group.SetMeshInstance(1, {draco::kInvalidMeshIndex, 0}); + mesh_group.SetMeshInstance(2, {draco::kInvalidMeshIndex, 0}); + + // Cleanup scene. + draco::SceneUtils::Cleanup(scene.get()); + + // Check cleaned up scene. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 1); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 2); + ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::kInvalidMeshGroupIndex); +} + +TEST(SceneUtilsTest, TestCleanupUnreferencedMeshGroup) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5); + + // Invalidate references to truck axle mesh group. + scene->GetNode(draco::SceneNodeIndex(2)) + ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex); + scene->GetNode(draco::SceneNodeIndex(4)) + ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex); + + // Cleanup scene. + draco::SceneUtils::Cleanup(scene.get()); + + // Check cleaned up scene. + ASSERT_EQ(scene->NumMeshes(), 3); + ASSERT_EQ(scene->NumMeshGroups(), 1); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 3); +} + +TEST(SceneUtilsTest, TestCleanupInvalidMeshIndex) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 5); + ASSERT_EQ(scene->GetNode(draco::SceneNodeIndex(0))->GetMeshGroupIndex(), + draco::MeshGroupIndex(0)); + + // Invalidate references to two truck body parts in mesh group. + draco::MeshGroup &mesh_group = *scene->GetMeshGroup(draco::MeshGroupIndex(0)); + ASSERT_EQ(mesh_group.NumMeshInstances(), 3); + mesh_group.SetMeshInstance(0, {draco::kInvalidMeshIndex, 0}); + mesh_group.SetMeshInstance(2, {draco::kInvalidMeshIndex, 0}); + + // Cleanup scene. + draco::SceneUtils::Cleanup(scene.get()); + + // Check cleaned up scene. + ASSERT_EQ(scene->NumMeshes(), 2); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 3); + ASSERT_EQ(scene->GetMeshGroup(draco::MeshGroupIndex(0))->NumMeshInstances(), + 1); +} + +TEST(SceneUtilsTest, TestCleanupUnusedNodes) { + auto scene = + draco::ReadSceneFromTestFile("CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumNodes(), 5); + + draco::SceneUtils::CleanupOptions options; + options.remove_unused_nodes = true; + + // Delete mesh on node 2 and try to remove unused nodes. + // Node 2 is connected to node 1 that has no mesh as well. But node 2 is also + // used in an animation so we don't actually expect anything to be deleted. + scene->GetNode(draco::SceneNodeIndex(2)) + ->SetMeshGroupIndex(draco::kInvalidMeshGroupIndex); + draco::SceneUtils::Cleanup(scene.get(), options); + + ASSERT_EQ(scene->NumNodes(), 5); + + // Now remove the animation channel that used the node and try it again. This + // time, we expect two nodes to be deleted (node 1 and node 2). Node 1 will be + // deleted because it doesn't contain a mesh and all its children are unused. + ASSERT_EQ(scene->GetAnimation(draco::AnimationIndex(0)) + ->GetChannel(0) + ->target_index, + 2); + // Change the mapped node to node 4 (we can't actually remove channel as of + // the time this test was written). + scene->GetAnimation(draco::AnimationIndex(0))->GetChannel(0)->target_index = + 4; + + // Cleanup again. + draco::SceneUtils::Cleanup(scene.get(), options); + ASSERT_EQ(scene->NumNodes(), 3); // Two nodes should be deleted. + + // Ensure all node indices are remapped to the new values. + for (draco::SceneNodeIndex sni(0); sni < scene->NumNodes(); ++sni) { + const auto *node = scene->GetNode(sni); + for (int i = 0; i < node->NumChildren(); ++i) { + ASSERT_LT(node->Child(i).value(), 3); + } + for (int i = 0; i < node->NumParents(); ++i) { + ASSERT_LT(node->Parent(i).value(), 3); + } + } + + // Ensure the animation channels are mapped to the updated node indices (node + // 4 should be new node 2 because two nodes were removed). + ASSERT_EQ(scene->GetAnimation(draco::AnimationIndex(0)) + ->GetChannel(0) + ->target_index, + 2); +} + +TEST(SceneUtilsTest, TestDeduplicateMeshGroups) { + // Input scene has four different mesh groups but only two of them should + // contain unique set of meshes. + auto scene = + draco::ReadSceneFromTestFile("DuplicateMeshes/duplicate_meshes.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 4); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 7); + + draco::SceneUtils::DeduplicateMeshGroups(scene.get()); + + // Check deduplicated scene. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(scene->NumMeshGroups(), 2); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 7); +} + +TEST(SceneUtilsTest, TestCleanupUnusedTexCoordsNoTextures) { + // The glTF file has two tex coords that are unused because the materials do + // not reference any textures. + auto scene = draco::ReadSceneFromTestFile("UnusedTexCoords/NoTextures.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 2); + + // Cleanup scene and check that unused UV are not removed by default. + draco::SceneUtils::Cleanup(scene.get()); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 2); + + // Cleanup scene and check that unused UV are removed when requested. + draco::SceneUtils::CleanupOptions options; + options.remove_unused_tex_coords = true; + draco::SceneUtils::Cleanup(scene.get(), options); + ASSERT_EQ(scene->GetMesh(draco::MeshIndex(0)) + .NumNamedAttributes(draco::GeometryAttribute::TEX_COORD), + 0); +} + +TEST(SceneUtilsTest, TestCleanupUnusedTexCoords0NoReferences) { + auto scene = draco::ReadSceneFromTestFile( + "UnusedTexCoords/TexCoord0InvalidTexCoord1Valid.gltf"); + ASSERT_NE(scene, nullptr); + typedef draco::GeometryAttribute Att; + + draco::Mesh &mesh = scene->GetMesh(draco::MeshIndex(0)); + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 2); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 14); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 1)->size(), 4); + auto &ml = scene->GetMaterialLibrary(); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 1); + + // Cleanup unused texture coordinate attributes. + draco::SceneUtils::CleanupOptions options; + options.remove_unused_tex_coords = true; + draco::SceneUtils::Cleanup(scene.get(), options); + + // Check that the unreferenced attribute was removed. + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 1); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 4); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 0); +} + +TEST(SceneUtilsTest, TestCleanupUnusedTexCoords1NoReferences) { + auto scene = draco::ReadSceneFromTestFile( + "UnusedTexCoords/TexCoord0ValidTexCoord1Invalid.gltf"); + ASSERT_NE(scene, nullptr); + typedef draco::GeometryAttribute Att; + + draco::Mesh &mesh = scene->GetMesh(draco::MeshIndex(0)); + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 2); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 14); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 1)->size(), 4); + auto &ml = scene->GetMaterialLibrary(); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 0); + + // Cleanup unused texture coordinate attributes. + draco::SceneUtils::CleanupOptions options; + options.remove_unused_tex_coords = true; + draco::SceneUtils::Cleanup(scene.get(), options); + + // Check that the unreferenced attribute was removed. + ASSERT_EQ(mesh.NumNamedAttributes(Att::TEX_COORD), 1); + ASSERT_EQ(mesh.GetNamedAttribute(Att::TEX_COORD, 0)->size(), 14); + ASSERT_EQ(ml.NumMaterials(), 1); + ASSERT_EQ(ml.GetMaterial(0)->NumTextureMaps(), 1); + ASSERT_EQ(ml.GetMaterial(0)->GetTextureMapByIndex(0)->tex_coord_index(), 0); +} + +TEST(SceneUtilsTest, TestComputeGlobalNodeTransform) { + // Tests that we can compute global transformation of scene nodes. + + auto scene = draco::ReadSceneFromTestFile("simple_skin.gltf"); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumNodes(), 3); + + // Compute and check global node transforms. + constexpr float kTolerance = 1e-6; + // clang-format off + AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform( + *scene, draco::SceneNodeIndex(0)), + Eigen::Matrix4d::Identity(), + kTolerance); + AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform( + *scene, draco::SceneNodeIndex(1)), + Eigen::Matrix4d{{1.0, 0.0, 0.0, 0.0}, + {0.0, 1.0, 0.0, 1.0}, + {0.0, 0.0, 1.0, 0.0}, + {0.0, 0.0, 0.0, 1.0}}, + kTolerance); + AssertMatrixNear(draco::SceneUtils::ComputeGlobalNodeTransform( + *scene, draco::SceneNodeIndex(2)), + Eigen::Matrix4d{{1.0, 0.0, 0.0, 0.0}, + {0.0, 1.0, 0.0, 1.0}, + {0.0, 0.0, 1.0, 0.0}, + {0.0, 0.0, 0.0, 1.0}}, + kTolerance); + // clang-format on +} + +TEST(SceneUtilsTest, TestIsDracoCompressionEnabled) { + // Tests that we can determine whether any of the scene meshes have geometry + // compression enabled. + const std::string file = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + auto scene = draco::ReadSceneFromTestFile(file); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + // Check that the scene has geometry compression disabled by default. + ASSERT_FALSE(draco::SceneUtils::IsDracoCompressionEnabled(*scene)); + + // Check that geometry compression can be enabled. + scene->GetMesh(MeshIndex(2)).SetCompressionEnabled(true); + ASSERT_TRUE(draco::SceneUtils::IsDracoCompressionEnabled(*scene)); +} + +TEST(SceneUtilsTest, TestSetDracoCompressionOptions) { + // Tests that geometry compression settings can be set for all scene meshes. + const std::string file = "CesiumMilkTruck/glTF/CesiumMilkTruck.gltf"; + auto scene = draco::ReadSceneFromTestFile(file); + ASSERT_NE(scene, nullptr); + ASSERT_EQ(scene->NumMeshes(), 4); + + // Check that compression is initially disabled for all scene meshes. + ASSERT_FALSE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled()); + + // Check that initially all scene meshes have default compression options. + draco::DracoCompressionOptions defaults; + ASSERT_EQ(scene->GetMesh(MeshIndex(0)).GetCompressionOptions(), defaults); + ASSERT_EQ(scene->GetMesh(MeshIndex(1)).GetCompressionOptions(), defaults); + ASSERT_EQ(scene->GetMesh(MeshIndex(2)).GetCompressionOptions(), defaults); + ASSERT_EQ(scene->GetMesh(MeshIndex(3)).GetCompressionOptions(), defaults); + + // Check geometry compression options can be set to all scene meshes and that + // this also enables compression for all scnene meshes. + draco::DracoCompressionOptions options; + options.compression_level = 10; + options.quantization_bits_normal = 12; + draco::SceneUtils::SetDracoCompressionOptions(&options, scene.get()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled()); + ASSERT_TRUE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled()); + ASSERT_EQ(scene->GetMesh(MeshIndex(0)).GetCompressionOptions(), options); + ASSERT_EQ(scene->GetMesh(MeshIndex(1)).GetCompressionOptions(), options); + ASSERT_EQ(scene->GetMesh(MeshIndex(2)).GetCompressionOptions(), options); + ASSERT_EQ(scene->GetMesh(MeshIndex(3)).GetCompressionOptions(), options); + + // Check that geometry compression can be disabled for all scene meshes. + draco::SceneUtils::SetDracoCompressionOptions(nullptr, scene.get()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(0)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(1)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(2)).IsCompressionEnabled()); + ASSERT_FALSE(scene->GetMesh(MeshIndex(3)).IsCompressionEnabled()); +} + +TEST(SceneUtilsTest, TestFindLargestBaseMeshTransforms) { + // Tests that FindLargestBaseMeshTransforms() works as expected. + auto scene = + draco::ReadSceneFromTestFile("CubeScaledInstances/glTF/cube_att.gltf"); + ASSERT_NE(scene, nullptr); + + // There should be one base mesh with four instances. + ASSERT_EQ(scene->NumMeshes(), 1); + ASSERT_EQ(draco::SceneUtils::ComputeAllInstances(*scene).size(), 4); + + const auto transforms = + draco::SceneUtils::FindLargestBaseMeshTransforms(*scene); + + ASSERT_EQ(transforms.size(), 1); // One transform for the single base mesh. + + // The largest instance should have a uniform scale 4. + const draco::MeshIndex mi(0); + ASSERT_EQ(transforms[mi].diagonal(), Eigen::Vector4d(4, 4, 4, 1)); +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/trs_matrix.cc b/contrib/draco/src/draco/scene/trs_matrix.cc new file mode 100644 index 000000000..6e6dac251 --- /dev/null +++ b/contrib/draco/src/draco/scene/trs_matrix.cc @@ -0,0 +1,102 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/trs_matrix.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void TrsMatrix::Copy(const TrsMatrix &tm) { + matrix_ = tm.matrix_; + translation_ = tm.translation_; + rotation_ = tm.rotation_; + scale_ = tm.scale_; + matrix_set_ = tm.matrix_set_; + translation_set_ = tm.translation_set_; + rotation_set_ = tm.rotation_set_; + scale_set_ = tm.scale_set_; +} + +Eigen::Matrix4d TrsMatrix::ComputeTransformationMatrix() const { + // Return transformation matrix if it has been set. + if (matrix_set_) { + return matrix_; + } + + // Populate translation matrix. + Eigen::Matrix4d translation_matrix = Eigen::Matrix4d::Identity(); + translation_matrix(0, 3) = translation_[0]; + translation_matrix(1, 3) = translation_[1]; + translation_matrix(2, 3) = translation_[2]; + + // Populate rotation matrix using rotation quaternion. + Eigen::Matrix3d rotation_matrix_3 = rotation_.normalized().toRotationMatrix(); + + // Convert the 3x3 matrix to a 4x4 matrix that can be multiplied with the + // other TRS matrices. + Eigen::Matrix4d rotation_matrix = Eigen::Matrix4d::Identity(); + rotation_matrix.block<3, 3>(0, 0) = rotation_matrix_3; + + // Populate scale matrix. + const Eigen::Matrix4d scale_matrix( + Eigen::Vector4d(scale_.x(), scale_.y(), scale_.z(), 1.0).asDiagonal()); + + // Return transformation matrix computed by combining TRS matrices. + return translation_matrix * rotation_matrix * scale_matrix; +} + +bool TrsMatrix::IsMatrixIdentity() const { + if (!matrix_set_) { + return true; + } + return matrix_ == Eigen::Matrix4d::Identity(); +} + +bool TrsMatrix::IsMatrixTranslationOnly() const { + if (!matrix_set_) { + return false; + } + Eigen::Matrix4d translation_check = matrix_; + translation_check(0, 3) = 0.0; + translation_check(1, 3) = 0.0; + translation_check(2, 3) = 0.0; + return translation_check == Eigen::Matrix4d::Identity(); +} + +bool TrsMatrix::operator==(const TrsMatrix &trs_matrix) const { + if (matrix_set_ != trs_matrix.matrix_set_ || + translation_set_ != trs_matrix.translation_set_ || + rotation_set_ != trs_matrix.rotation_set_ || + scale_set_ != trs_matrix.scale_set_) { + return false; + } + if (matrix_set_ && matrix_ != trs_matrix.matrix_) { + return false; + } + if (translation_set_ && translation_ != trs_matrix.translation_) { + return false; + } + if (rotation_set_ && rotation_ != trs_matrix.rotation_) { + return false; + } + if (scale_set_ && scale_set_ != trs_matrix.scale_set_) { + return false; + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/scene/trs_matrix.h b/contrib/draco/src/draco/scene/trs_matrix.h new file mode 100644 index 000000000..6c2ab7388 --- /dev/null +++ b/contrib/draco/src/draco/scene/trs_matrix.h @@ -0,0 +1,124 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_SCENE_TRS_MATRIX_H_ +#define DRACO_SCENE_TRS_MATRIX_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "Eigen/Geometry" +#include "draco/core/status_or.h" + +namespace draco { + +// This class is used to store one or more of a translation, rotation, scale +// vectors or a transformation matrix. +class TrsMatrix { + public: + TrsMatrix() + : matrix_(Eigen::Matrix4d::Identity()), + translation_(0.0, 0.0, 0.0), + rotation_(1.0, 0.0, 0.0, 0.0), // (w, x, y, z) + scale_(1.0, 1.0, 1.0), + matrix_set_(false), + translation_set_(false), + rotation_set_(false), + scale_set_(false) {} + + void Copy(const TrsMatrix &tm); + + void SetMatrix(const Eigen::Matrix4d &matrix) { + matrix_ = matrix; + matrix_set_ = true; + } + bool MatrixSet() const { return matrix_set_; } + StatusOr Matrix() const { + if (!matrix_set_) { + return Status(Status::DRACO_ERROR, "Matrix is not set."); + } + return matrix_; + } + + void SetTranslation(const Eigen::Vector3d &translation) { + translation_ = translation; + translation_set_ = true; + } + bool TranslationSet() const { return translation_set_; } + StatusOr Translation() const { + if (!translation_set_) { + return Status(Status::DRACO_ERROR, "Translation is not set."); + } + return translation_; + } + + void SetRotation(const Eigen::Quaterniond &rotation) { + rotation_ = rotation; + rotation_set_ = true; + } + bool RotationSet() const { return rotation_set_; } + StatusOr Rotation() const { + if (!rotation_set_) { + return Status(Status::DRACO_ERROR, "Rotation is not set."); + } + return rotation_; + } + + void SetScale(const Eigen::Vector3d &scale) { + scale_ = scale; + scale_set_ = true; + } + bool ScaleSet() const { return scale_set_; } + StatusOr Scale() const { + if (!scale_set_) { + return Status(Status::DRACO_ERROR, "Scale is not set."); + } + return scale_; + } + + // Returns true if the matrix is not set or if matrix is set and is equal to + // identity. + bool IsMatrixIdentity() const; + + // Returns true if matrix is set and only the translation elements may differ + // from identity. Returns false if matrix is not set. + bool IsMatrixTranslationOnly() const; + + // Returns transformation matrix if it has been set. Otherwise, computes + // transformation matrix from TRS vectors and returns it. + Eigen::Matrix4d ComputeTransformationMatrix() const; + + // Returns a boolean indicating whether any of the transforms have been set. + // Can be used to check whether this object represents a default transform. + bool TransformSet() const { + return matrix_set_ || translation_set_ || rotation_set_ || scale_set_; + } + + bool operator==(const TrsMatrix &trs_matrix) const; + + private: + Eigen::Matrix4d matrix_; + Eigen::Vector3d translation_; + Eigen::Quaterniond rotation_; + Eigen::Vector3d scale_; + bool matrix_set_; + bool translation_set_; + bool rotation_set_; + bool scale_set_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_SCENE_TRS_MATRIX_H_ diff --git a/contrib/draco/src/draco/scene/trs_matrix_test.cc b/contrib/draco/src/draco/scene/trs_matrix_test.cc new file mode 100644 index 000000000..d7938e974 --- /dev/null +++ b/contrib/draco/src/draco/scene/trs_matrix_test.cc @@ -0,0 +1,79 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/scene/trs_matrix.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" + +namespace { + +#ifdef DRACO_TRANSCODER_SUPPORTED + +TEST(TrsMatrixTest, TestIsMatrixIdentity) { + draco::TrsMatrix trs; + ASSERT_EQ(trs.MatrixSet(), false); + ASSERT_EQ(trs.IsMatrixIdentity(), true); + + // clang-format off + Eigen::Matrix4d matrix; + matrix << 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16; + // clang-format on + trs.SetMatrix(matrix); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixIdentity(), false); + + trs.SetMatrix(Eigen::Matrix4d::Identity()); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixIdentity(), true); +} + +TEST(TrsMatrixTest, TestIsMatrixTranslationOnly) { + draco::TrsMatrix trs; + ASSERT_EQ(trs.MatrixSet(), false); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), false); + + trs.SetMatrix(Eigen::Matrix4d::Identity()); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), true); + + // clang-format off + Eigen::Matrix4d matrix; + matrix << 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16; + // clang-format on + trs.SetMatrix(matrix); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), false); + + // clang-format off + Eigen::Matrix4d translation; + translation << 1, 0, 0, 1, + 0, 1, 0, 2, + 0, 0, 1, 3, + 0, 0, 0, 1; + // clang-format on + trs.SetMatrix(translation); + ASSERT_EQ(trs.MatrixSet(), true); + ASSERT_EQ(trs.IsMatrixTranslationOnly(), true); +} + +#endif // DRACO_TRANSCODER_SUPPORTED + +} // namespace diff --git a/contrib/draco/src/draco/texture/source_image.cc b/contrib/draco/src/draco/texture/source_image.cc new file mode 100644 index 000000000..b4d493250 --- /dev/null +++ b/contrib/draco/src/draco/texture/source_image.cc @@ -0,0 +1,29 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/source_image.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void SourceImage::Copy(const SourceImage &src) { + mime_type_ = src.mime_type_; + filename_ = src.filename_; + encoded_data_ = src.encoded_data_; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/source_image.h b/contrib/draco/src/draco/texture/source_image.h new file mode 100644 index 000000000..5827918e4 --- /dev/null +++ b/contrib/draco/src/draco/texture/source_image.h @@ -0,0 +1,72 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TEXTURE_SOURCE_IMAGE_H_ +#define DRACO_TEXTURE_SOURCE_IMAGE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/core/status.h" + +namespace draco { + +// This class is used to hold the encoded and decoded data and characteristics +// for an image. In order for the image to contain "valid" encoded data, either +// the |filename_| must point to a valid image file or the |mime_type_| and +// |encoded_data_| must contain valid image data. +class SourceImage { + public: + SourceImage() {} + + // No copy constructors. + SourceImage(const SourceImage &) = delete; + SourceImage &operator=(const SourceImage &) = delete; + // No move constructors. + SourceImage(SourceImage &&) = delete; + SourceImage &operator=(SourceImage &&) = delete; + + void Copy(const SourceImage &src); + + // Sets the name of the source image file. + void set_filename(const std::string &filename) { filename_ = filename; } + const std::string &filename() const { return filename_; } + + void set_mime_type(const std::string &mime_type) { mime_type_ = mime_type; } + const std::string &mime_type() const { return mime_type_; } + + std::vector &MutableEncodedData() { return encoded_data_; } + const std::vector &encoded_data() const { return encoded_data_; } + + private: + // The filename of the image. This field can be empty as long as |mime_type_| + // and |encoded_data_| is not empty. + std::string filename_; + + // The mimetype of the |encoded_data_|. + std::string mime_type_; + + // The encoded data of the image. This field can be empty as long as + // |filename_| is not empty. + std::vector encoded_data_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_SOURCE_IMAGE_H_ diff --git a/contrib/draco/src/draco/texture/texture.h b/contrib/draco/src/draco/texture/texture.h new file mode 100644 index 000000000..1d3b6e382 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture.h @@ -0,0 +1,46 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TEXTURE_TEXTURE_H_ +#define DRACO_TEXTURE_TEXTURE_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +#include "draco/io/image_compression_options.h" +#include "draco/texture/source_image.h" + +namespace draco { + +// Texture class storing the source image data. +class Texture { + public: + void Copy(Texture &other) { source_image_.Copy(other.source_image_); } + + void set_source_image(const SourceImage &image) { source_image_.Copy(image); } + const SourceImage &source_image() const { return source_image_; } + SourceImage &source_image() { return source_image_; } + + private: + // If set this is the image that this texture is based from. + SourceImage source_image_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_H_ diff --git a/contrib/draco/src/draco/texture/texture_library.cc b/contrib/draco/src/draco/texture/texture_library.cc new file mode 100644 index 000000000..221ff28d4 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_library.cc @@ -0,0 +1,61 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/texture_library.h" + +#include + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +void TextureLibrary::Copy(const TextureLibrary &src) { + Clear(); + Append(src); +} + +void TextureLibrary::Append(const TextureLibrary &src) { + const size_t old_num_textures = textures_.size(); + textures_.resize(old_num_textures + src.textures_.size()); + for (int i = 0; i < src.textures_.size(); ++i) { + textures_[old_num_textures + i] = std::unique_ptr(new Texture()); + textures_[old_num_textures + i]->Copy(*src.textures_[i]); + } +} + +void TextureLibrary::Clear() { textures_.clear(); } + +int TextureLibrary::PushTexture(std::unique_ptr texture) { + textures_.push_back(std::move(texture)); + return textures_.size() - 1; +} + +std::unordered_map +TextureLibrary::ComputeTextureToIndexMap() const { + std::unordered_map ret; + for (int i = 0; i < textures_.size(); ++i) { + ret[textures_[i].get()] = i; + } + return ret; +} + +std::unique_ptr TextureLibrary::RemoveTexture(int index) { + std::unique_ptr ret = std::move(textures_[index]); + textures_.erase(textures_.begin() + index); + return ret; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_library.h b/contrib/draco/src/draco/texture/texture_library.h new file mode 100644 index 000000000..a377d8fbc --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_library.h @@ -0,0 +1,67 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TEXTURE_TEXTURE_LIBRARY_H_ +#define DRACO_TEXTURE_TEXTURE_LIBRARY_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include + +#include "draco/texture/texture.h" + +namespace draco { + +// Container class for storing draco::Texture objects in an indexed list. +class TextureLibrary { + public: + // Copies textures from the source library to this library. Order of the + // copied textures is preserved. + void Copy(const TextureLibrary &src); + + // Appends all textures from the source library to this library. All textures + // are copied over. + void Append(const TextureLibrary &src); + + // Removes all textures from the library. + void Clear(); + + // Pushes a new texture into the library. Returns an index of the newly + // inserted texture. + int PushTexture(std::unique_ptr texture); + + size_t NumTextures() const { return textures_.size(); } + + Texture *GetTexture(int index) { return textures_[index].get(); } + const Texture *GetTexture(int index) const { return textures_[index].get(); } + + // Returns a map from texture pointer to texture index for all textures. + std::unordered_map ComputeTextureToIndexMap() const; + + // Removes and returns a texture from the library. The returned texture can be + // either used by the caller or ignored in which case it would be + // automatically deleted. + std::unique_ptr RemoveTexture(int index); + + private: + std::vector> textures_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_LIBRARY_H_ diff --git a/contrib/draco/src/draco/texture/texture_library_test.cc b/contrib/draco/src/draco/texture/texture_library_test.cc new file mode 100644 index 000000000..4d681fdd2 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_library_test.cc @@ -0,0 +1,22 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/texture_library.h" + +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" + +namespace {} // namespace diff --git a/contrib/draco/src/draco/texture/texture_map.cc b/contrib/draco/src/draco/texture/texture_map.cc new file mode 100644 index 000000000..459d3f600 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_map.cc @@ -0,0 +1,86 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/texture_map.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +TextureMap::TextureMap() + : type_(TextureMap::GENERIC), + wrapping_mode_(CLAMP_TO_EDGE), + tex_coord_index_(-1), + min_filter_(UNSPECIFIED), + mag_filter_(UNSPECIFIED), + texture_(nullptr) {} + +void TextureMap::Copy(const TextureMap &src) { + type_ = src.type_; + wrapping_mode_ = src.wrapping_mode_; + tex_coord_index_ = src.tex_coord_index_; + min_filter_ = src.min_filter_; + mag_filter_ = src.mag_filter_; + if (src.owned_texture_ == nullptr) { + owned_texture_ = nullptr; + texture_ = src.texture_; + } else { + std::unique_ptr new_texture(new Texture()); + new_texture->Copy(*src.owned_texture_); + owned_texture_ = std::move(new_texture); + texture_ = owned_texture_.get(); + } + texture_transform_.Copy(src.texture_transform_); +} + +void TextureMap::SetProperties(Type type) { + SetProperties(type, WrappingMode(CLAMP_TO_EDGE), 0); +} + +void TextureMap::SetProperties(TextureMap::Type type, int tex_coord_index) { + SetProperties(type, WrappingMode(CLAMP_TO_EDGE), tex_coord_index); +} + +void TextureMap::SetProperties(Type type, WrappingMode wrapping_mode, + int tex_coord_index) { + SetProperties(type, wrapping_mode, tex_coord_index, UNSPECIFIED, UNSPECIFIED); +} + +void TextureMap::SetProperties(Type type, WrappingMode wrapping_mode, + int tex_coord_index, FilterType min_filter, + FilterType mag_filter) { + type_ = type; + wrapping_mode_ = wrapping_mode; + tex_coord_index_ = tex_coord_index; + min_filter_ = min_filter; + mag_filter_ = mag_filter; +} + +void TextureMap::SetTexture(std::unique_ptr texture) { + owned_texture_ = std::move(texture); + texture_ = owned_texture_.get(); +} + +void TextureMap::SetTexture(Texture *texture) { + owned_texture_ = nullptr; + texture_ = texture; +} + +void TextureMap::SetTransform(const TextureTransform &transform) { + texture_transform_.Copy(transform); +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_map.h b/contrib/draco/src/draco/texture/texture_map.h new file mode 100644 index 000000000..f3a95b501 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_map.h @@ -0,0 +1,175 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TEXTURE_TEXTURE_MAP_H_ +#define DRACO_TEXTURE_TEXTURE_MAP_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/texture/texture.h" +#include "draco/texture/texture_transform.h" + +namespace draco { + +// Class representing mapping of one texture to a mesh. A texture map +// specifies the mesh attribute that contains texture coordinates used by the +// texture. The class also defines an intended use of the texture as a so called +// mapping type (COLOR, NORMAL_TANGENT_SPACE, etc..). Mapping types are roughly +// based on GLTF 2.0 material spec that describes a metallic-roughness PBR +// material model. More details can be found here: +// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materials +class TextureMap { + public: + enum Type { + // Generic purpose texture (not GLTF compliant). + GENERIC = 0, + // Color data with optional alpha channel for transparency (GLTF compliant). + COLOR = 1, + // Dedicated texture for storing transparency (not GLTF compliant). + OPACITY = 2, + // Dedicated texture for storing metallic property (not GLTF compliant). + METALLIC = 3, + // Dedicated texture for storing roughness property (not GLTF compliant). + ROUGHNESS = 4, + // Combined texture for storing metallic and roughness properties. + // B == metallic, G == roughness (GLTF compliant). + METALLIC_ROUGHNESS = 5, + // Normal map defined in the object space of the mesh (not GLTF compliant). + NORMAL_OBJECT_SPACE = 6, + // Normal map defined in the tangent space of the mesh (GLTF compliant). + NORMAL_TANGENT_SPACE = 7, + // Precomputed ambient occlusion on the surface (GLTF compliant). + AMBIENT_OCCLUSION = 8, + // Emissive color (GLTF compliant). + EMISSIVE = 9, + // Texture types of glTF material extension KHR_materials_sheen. + SHEEN_COLOR = 10, + SHEEN_ROUGHNESS = 11, + // Texture types of glTF material extension KHR_materials_transmission. + TRANSMISSION = 12, + // Texture types of glTF material extension KHR_materials_clearcoat. + CLEARCOAT = 13, + CLEARCOAT_ROUGHNESS = 14, + CLEARCOAT_NORMAL = 15, + // Texture types of glTF material extension KHR_materials_volume. + THICKNESS = 16, + // Texture types of glTF material extension KHR_materials_specular. + SPECULAR = 17, + SPECULAR_COLOR = 18, + // The number of texture types. + TEXTURE_TYPES_COUNT + }; + + enum AxisWrappingMode { + // Out of bounds access along a texture axis should be clamped to the + // nearest edge (default). + CLAMP_TO_EDGE = 0, + // Texture is repeated along a texture axis in a mirrored pattern. + MIRRORED_REPEAT, + // Texture is repeated along a texture axis (tiled textures). + REPEAT + }; + + struct WrappingMode { + explicit WrappingMode(AxisWrappingMode mode) : WrappingMode(mode, mode) {} + WrappingMode(AxisWrappingMode s, AxisWrappingMode t) : s(s), t(t) {} + AxisWrappingMode s; + AxisWrappingMode t; + }; + + // Filter types are roughly based on glTF 2.0 samplers spec. + // https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#samplers + enum FilterType { + UNSPECIFIED = 0, + NEAREST, + LINEAR, + NEAREST_MIPMAP_NEAREST, + LINEAR_MIPMAP_NEAREST, + NEAREST_MIPMAP_LINEAR, + LINEAR_MIPMAP_LINEAR + }; + + TextureMap(); + TextureMap(TextureMap &&) = default; + + // Copies texture map data from the |src| texture map to this texture map. + void Copy(const TextureMap &src); + + // Sets the mapping information between the texture and the target mesh. + // |tex_coord_index| is the local index of the texture coordinates that is + // used to map the texture on the mesh. + void SetProperties(Type type); + void SetProperties(Type type, int tex_coord_index); + void SetProperties(Type type, WrappingMode wrapping_mode, + int tex_coord_index); + void SetProperties(Type type, WrappingMode wrapping_mode, int tex_coord_index, + FilterType min_filter, FilterType mag_filter); + + // Set texture and transfer its ownership to the TextureMap object. + // + // Note that this should not be used if this TextureMap is part of a + // MaterialLibrary. For such cases, the TextureMap's texture should refer to + // an entry in the MaterialLibrary's TextureLibrary. + void SetTexture(std::unique_ptr texture); + + // Set texture and without transferring the ownership. The caller needs to + // ensure the texture is valid during the lifetime of the TextureMap object. + void SetTexture(Texture *texture); + + void SetTransform(const TextureTransform &transform); + const TextureTransform &texture_transform() const { + return texture_transform_; + } + + const Texture *texture() const { return texture_; } + Texture *texture() { return texture_; } + Type type() const { return type_; } + WrappingMode wrapping_mode() const { return wrapping_mode_; } + int tex_coord_index() const { return tex_coord_index_; } + FilterType min_filter() const { return min_filter_; } + FilterType mag_filter() const { return mag_filter_; } + + TextureMap &operator=(TextureMap &&) = default; + + private: + Type type_; + WrappingMode wrapping_mode_; + + // Local index of the texture coordinates that is used to map the texture on + // the mesh. For example, |tex_coord_index_ == 0| would correspond to the + // first TEX_COORD attribute of the mesh. + int tex_coord_index_; + + FilterType min_filter_; + FilterType mag_filter_; + + // Used when the texture object is owned by TextureMap, otherwise set to + // nullptr. + std::unique_ptr owned_texture_; + + // Either raw pointer owned by |owned_texture_| or a pointer to a user + // specified texture in case |owned_texture_| is nullptr. + Texture *texture_; + + // Transformation values of the texture map. + TextureTransform texture_transform_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_MAP_H_ diff --git a/contrib/draco/src/draco/texture/texture_map_test.cc b/contrib/draco/src/draco/texture/texture_map_test.cc new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/draco/src/draco/texture/texture_transform.cc b/contrib/draco/src/draco/texture/texture_transform.cc new file mode 100644 index 000000000..ccb00d59f --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_transform.cc @@ -0,0 +1,79 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/texture_transform.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED + +namespace draco { + +TextureTransform::TextureTransform() { Clear(); } + +void TextureTransform::Clear() { + offset_ = TextureTransform::GetDefaultOffset(); + rotation_ = TextureTransform::GetDefaultRotation(); + scale_ = TextureTransform::GetDefaultScale(); + tex_coord_ = TextureTransform::GetDefaultTexCoord(); +} + +void TextureTransform::Copy(const TextureTransform &src) { + offset_ = src.offset_; + rotation_ = src.rotation_; + scale_ = src.scale_; + tex_coord_ = src.tex_coord_; +} + +bool TextureTransform::IsDefault(const TextureTransform &tt) { + const TextureTransform defaults; + if (tt == defaults) { + return true; + } + return false; +} + +bool TextureTransform::IsOffsetSet() const { + return offset_ != TextureTransform::GetDefaultOffset(); +} + +bool TextureTransform::IsRotationSet() const { + return rotation_ != TextureTransform::GetDefaultRotation(); +} + +bool TextureTransform::IsScaleSet() const { + return scale_ != TextureTransform::GetDefaultScale(); +} + +bool TextureTransform::IsTexCoordSet() const { + return tex_coord_ != TextureTransform::GetDefaultTexCoord(); +} + +bool TextureTransform::operator==(const TextureTransform &tt) const { + if (tex_coord_ != tt.tex_coord_) { + return false; + } + if (rotation_ != tt.rotation_) { + return false; + } + if (offset_ != tt.offset_) { + return false; + } + if (scale_ != tt.scale_) { + return false; + } + return true; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_transform.h b/contrib/draco/src/draco/texture/texture_transform.h new file mode 100644 index 000000000..b2ec47f2e --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_transform.h @@ -0,0 +1,75 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TEXTURE_TEXTURE_TRANSFORM_H_ +#define DRACO_TEXTURE_TEXTURE_TRANSFORM_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include + +namespace draco { + +// Class to hold texture transformations. Parameters are based on the glTF 2.0 +// extension KHR_texture_transform: +// https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_texture_transform +class TextureTransform { + public: + TextureTransform(); + + // Resets the values back to defaults. + void Clear(); + + // Copies texture transform data from the |src| texture transform to this + // texture transform. + void Copy(const TextureTransform &src); + + // Returns true if |tt| contains all default values. + static bool IsDefault(const TextureTransform &tt); + + bool IsOffsetSet() const; + bool IsRotationSet() const; + bool IsScaleSet() const; + bool IsTexCoordSet() const; + + void set_offset(const std::array &offset) { offset_ = offset; } + const std::array &offset() const { return offset_; } + void set_scale(const std::array &scale) { scale_ = scale; } + const std::array &scale() const { return scale_; } + + void set_rotation(double rotation) { rotation_ = rotation; } + double rotation() const { return rotation_; } + void set_tex_coord(int tex_coord) { tex_coord_ = tex_coord; } + int tex_coord() const { return tex_coord_; } + + bool operator==(const TextureTransform &tt) const; + + private: + static std::array GetDefaultOffset() { return {0.0, 0.0}; } + static float GetDefaultRotation() { return 0.0; } + static std::array GetDefaultScale() { return {0.0, 0.0}; } + static int GetDefaultTexCoord() { return -1; } + + std::array offset_; + double rotation_; + std::array scale_; + int tex_coord_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_TRANSFORM_H_ diff --git a/contrib/draco/src/draco/texture/texture_transform_test.cc b/contrib/draco/src/draco/texture/texture_transform_test.cc new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/draco/src/draco/texture/texture_utils.cc b/contrib/draco/src/draco/texture/texture_utils.cc new file mode 100644 index 000000000..7598ac780 --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_utils.cc @@ -0,0 +1,144 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/texture_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include +#include +#include +#include +#include +#include +#include + +namespace draco { + +std::string TextureUtils::GetTargetStem(const Texture &texture) { + // Return stem of the source image if there is one. + if (!texture.source_image().filename().empty()) { + const std::string &full_path = texture.source_image().filename(); + std::string folder_path; + std::string filename; + SplitPath(full_path, &folder_path, &filename); + return RemoveFileExtension(filename); + } + + // Return an empty stem. + return ""; +} + +std::string TextureUtils::GetOrGenerateTargetStem(const Texture &texture, + int index, + const std::string &suffix) { + // Return target stem from |texture| if there is one. + const std::string name = GetTargetStem(texture); + if (!name.empty()) { + return name; + } + + // Return target stem generated from |index| and |suffix|. + return "Texture" + std::to_string(index) + suffix; +} + +ImageFormat TextureUtils::GetTargetFormat(const Texture &texture) { + // Return format based on source image mime type. + return GetSourceFormat(texture); +} + +std::string TextureUtils::GetTargetExtension(const Texture &texture) { + return GetExtension(GetTargetFormat(texture)); +} + +ImageFormat TextureUtils::GetSourceFormat(const Texture &texture) { + // Try to get the extension based on source image mime type. + std::string extension = + LowercaseMimeTypeExtension(texture.source_image().mime_type()); + if (extension.empty() && !texture.source_image().filename().empty()) { + // Try to get the extension from the source image filename. + extension = LowercaseFileExtension(texture.source_image().filename()); + } + if (extension.empty()) { + // Default to png. + extension = "png"; + } + return GetFormat(extension); +} + +ImageFormat TextureUtils::GetFormat(const std::string &extension) { + if (extension == "png") { + return ImageFormat::PNG; + } else if (extension == "jpg" || extension == "jpeg") { + return ImageFormat::JPEG; + } else if (extension == "basis" || extension == "ktx2") { + return ImageFormat::BASIS; + } else if (extension == "webp") { + return ImageFormat::WEBP; + } + return ImageFormat::NONE; +} + +std::string TextureUtils::GetExtension(ImageFormat format) { + switch (format) { + case ImageFormat::PNG: + return "png"; + case ImageFormat::JPEG: + return "jpg"; + case ImageFormat::BASIS: + return "ktx2"; + case ImageFormat::WEBP: + return "webp"; + case ImageFormat::NONE: + default: + return ""; + } +} + +int TextureUtils::ComputeRequiredNumChannels( + const Texture &texture, const MaterialLibrary &material_library) { + // TODO(vytyaz): Consider a case where |texture| is not only used in OMR but + // also in other texture map types. + const auto mr_textures = TextureUtils::FindTextures( + TextureMap::METALLIC_ROUGHNESS, &material_library); + if (std::find(mr_textures.begin(), mr_textures.end(), &texture) == + mr_textures.end()) { + // Occlusion-only texture. + return 1; + } + // Occlusion-metallic-roughness texture. + return 3; +} + +std::vector TextureUtils::FindTextures( + const TextureMap::Type texture_type, + const MaterialLibrary *material_library) { + // Find textures with no duplicates. + std::unordered_set textures; + for (int i = 0; i < material_library->NumMaterials(); ++i) { + const TextureMap *const texture_map = + material_library->GetMaterial(i)->GetTextureMapByType(texture_type); + if (texture_map != nullptr && texture_map->texture() != nullptr) { + textures.insert(texture_map->texture()); + } + } + + // Return the textures as a vector. + std::vector result; + result.insert(result.end(), textures.begin(), textures.end()); + return result; +} + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/texture/texture_utils.h b/contrib/draco/src/draco/texture/texture_utils.h new file mode 100644 index 000000000..18d29950a --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_utils.h @@ -0,0 +1,78 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TEXTURE_TEXTURE_UTILS_H_ +#define DRACO_TEXTURE_TEXTURE_UTILS_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status.h" +#include "draco/core/status_or.h" +#include "draco/io/file_utils.h" +#include "draco/material/material_library.h" +#include "draco/texture/texture_library.h" +#include "draco/texture/texture_map.h" + +namespace draco { + +// Helper class implementing various utilities operating on draco::Texture. +class TextureUtils { + public: + // Returns |texture| image stem (file basename without extension) based on the + // source image filename or an empty string when source image is not set. + static std::string GetTargetStem(const Texture &texture); + + // Returns |texture| image stem (file basename without extension) based on the + // source image filename or a name generated from |index| and |suffix| like + // "Texture5_BaseColor" when source image is not set. + static std::string GetOrGenerateTargetStem(const Texture &texture, int index, + const std::string &suffix); + + // Returns |texture| format based on compression settings, the source image + // mime type or the source image filename. + static ImageFormat GetTargetFormat(const Texture &texture); + + // Returns |texture| image file extension based on compression settings, the + // source image mime type or the source image filename. + static std::string GetTargetExtension(const Texture &texture); + + // Returns |texture| format based on source image mime type or the source + // image filename. + static ImageFormat GetSourceFormat(const Texture &texture); + + // Returns image format corresponding to a given image file |extension|. NONE + // is returned when |extension| is empty or unknown. + static ImageFormat GetFormat(const std::string &extension); + + // Returns image file extension corresponding to a given image |format|. Empty + // extension is returned when the |format| is NONE. + static std::string GetExtension(ImageFormat format); + + // Returns the number of channels required for encoding a |texture| from a + // given |material_library|, taking into account texture opacity and assuming + // that occlusion and metallic-roughness texture maps may share a texture. + // TODO(vytyaz): Move this and FindTextures() to MaterialLibrary class. + static int ComputeRequiredNumChannels( + const Texture &texture, const MaterialLibrary &material_library); + + static std::vector FindTextures( + const TextureMap::Type texture_type, + const MaterialLibrary *material_library); +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TEXTURE_TEXTURE_UTILS_H_ diff --git a/contrib/draco/src/draco/texture/texture_utils_test.cc b/contrib/draco/src/draco/texture/texture_utils_test.cc new file mode 100644 index 000000000..45873ea7a --- /dev/null +++ b/contrib/draco/src/draco/texture/texture_utils_test.cc @@ -0,0 +1,163 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/texture/texture_utils.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/core/draco_test_utils.h" +#include "draco/io/texture_io.h" +#include "draco/texture/color_utils.h" + +namespace { + +TEST(TextureUtilsTest, TestGetTargetNameForTextureLoadedFromFile) { + // Tests that correct target stem and format are returned by texture utils for + // texture loaded from image file (stem and format from source file). + std::unique_ptr texture = + draco::ReadTextureFromFile(draco::GetTestFileFullPath("fast.jpg")) + .value(); + ASSERT_NE(texture, nullptr); + ASSERT_EQ(draco::TextureUtils::GetTargetStem(*texture), "fast"); + ASSERT_EQ(draco::TextureUtils::GetTargetExtension(*texture), "jpg"); + ASSERT_EQ(draco::TextureUtils::GetTargetFormat(*texture), + draco::ImageFormat::JPEG); + ASSERT_EQ(draco::TextureUtils::GetOrGenerateTargetStem(*texture, 5, "_Color"), + "fast"); +} + +TEST(TextureUtilsTest, TestGetTargetNameForNewTexture) { + // Tests that correct target stem and format are returned by texture utils for + // a newly created texture (empty stem and PNG image type by default). + std::unique_ptr texture(new draco::Texture()); + ASSERT_NE(texture, nullptr); + ASSERT_EQ(draco::TextureUtils::GetTargetStem(*texture), ""); + ASSERT_EQ(draco::TextureUtils::GetOrGenerateTargetStem(*texture, 5, "_Color"), + "Texture5_Color"); + ASSERT_EQ(draco::TextureUtils::GetTargetExtension(*texture), "png"); + ASSERT_EQ(draco::TextureUtils::GetTargetFormat(*texture), + draco::ImageFormat::PNG); +} + +TEST(TextureUtilsTest, TestGetSourceFormat) { + // Tests that the source format is determined correctly for new textures and + // for textures loaded from file. + std::unique_ptr new_texture(new draco::Texture()); + DRACO_ASSIGN_OR_ASSERT( + std::unique_ptr png_texture, + draco::ReadTextureFromFile(draco::GetTestFileFullPath("test.png"))); + DRACO_ASSIGN_OR_ASSERT( + std::unique_ptr jpg_texture, + draco::ReadTextureFromFile(draco::GetTestFileFullPath("fast.jpg"))); + + // Check source formats. + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*new_texture), + draco::ImageFormat::PNG); + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*png_texture), + draco::ImageFormat::PNG); + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*jpg_texture), + draco::ImageFormat::JPEG); + + // Remove the mime-type from the jpeg texture and ensure the source format is + // still detected properly based on the filename. + jpg_texture->source_image().set_mime_type(""); + ASSERT_EQ(draco::TextureUtils::GetSourceFormat(*jpg_texture), + draco::ImageFormat::JPEG); +} + +TEST(TextureUtilsTest, TestGetFormat) { + typedef draco::ImageFormat ImageFormat; + ASSERT_EQ(draco::TextureUtils::GetFormat("png"), ImageFormat::PNG); + ASSERT_EQ(draco::TextureUtils::GetFormat("jpg"), ImageFormat::JPEG); + ASSERT_EQ(draco::TextureUtils::GetFormat("jpeg"), ImageFormat::JPEG); + ASSERT_EQ(draco::TextureUtils::GetFormat("basis"), ImageFormat::BASIS); + ASSERT_EQ(draco::TextureUtils::GetFormat("ktx2"), ImageFormat::BASIS); + ASSERT_EQ(draco::TextureUtils::GetFormat("webp"), ImageFormat::WEBP); + ASSERT_EQ(draco::TextureUtils::GetFormat(""), ImageFormat::NONE); + ASSERT_EQ(draco::TextureUtils::GetFormat("bmp"), ImageFormat::NONE); +} + +TEST(TextureUtilsTest, TestGetExtension) { + typedef draco::ImageFormat ImageFormat; + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::PNG), "png"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::JPEG), "jpg"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::BASIS), "ktx2"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::WEBP), "webp"); + ASSERT_EQ(draco::TextureUtils::GetExtension(ImageFormat::NONE), ""); +} + +TEST(TextureUtilsTest, TestComputeRequiredNumChannels) { + // Tests that the number of texture channels can be computed. Material library + // under test is created programmatically. + + // Load textures. + DRACO_ASSIGN_OR_ASSERT( + auto texture0, draco::ReadTextureFromFile( + draco::GetTestFileFullPath("fully_transparent.png"))); + ASSERT_NE(texture0, nullptr); + draco::Texture *texture0_ptr = texture0.get(); + DRACO_ASSIGN_OR_ASSERT( + auto texture1, + draco::ReadTextureFromFile(draco::GetTestFileFullPath("squares.png"))); + ASSERT_NE(texture1, nullptr); + const draco::Texture *texture1_ptr = texture1.get(); + DRACO_ASSIGN_OR_ASSERT( + auto texture2, draco::ReadTextureFromFile( + draco::GetTestFileFullPath("fully_transparent.png"))); + ASSERT_NE(texture2, nullptr); + const draco::Texture *texture2_ptr = texture2.get(); + + // Compute number of channels for occlusion-only texture. + draco::MaterialLibrary library; + draco::Material *const material0 = library.MutableMaterial(0); + material0->SetTextureMap(std::move(texture0), + draco::TextureMap::AMBIENT_OCCLUSION, 0); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture0_ptr, library), + 1); + + // Compute number of channels for occlusion-only texture with MR present but + // not using the same texture. + draco::Material *const material1 = library.MutableMaterial(1); + material1->SetTextureMap(std::move(texture1), + draco::TextureMap::METALLIC_ROUGHNESS, 0); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture0_ptr, library), + 1); + + // Compute number of channels for metallic-roughness texture. + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture1_ptr, library), + 3); + + // Compute number of channels texture that is used for occlusin map in one + // material and also shared with metallic-roughness map in another material. + draco::Material *const material2 = library.MutableMaterial(2); + DRACO_ASSERT_OK(material2->SetTextureMap( + texture0_ptr, draco::TextureMap::METALLIC_ROUGHNESS, 0)); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture0_ptr, library), + 3); + + // Compute number of channels for non-opaque texture. + material0->SetTextureMap(std::move(texture2), draco::TextureMap::COLOR, 0); + ASSERT_EQ( + draco::TextureUtils::ComputeRequiredNumChannels(*texture2_ptr, library), + 4); +} + +} // namespace + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/tools/draco_decoder.cc b/contrib/draco/src/draco/tools/draco_decoder.cc index 610709d62..cf5f18094 100644 --- a/contrib/draco/src/draco/tools/draco_decoder.cc +++ b/contrib/draco/src/draco/tools/draco_decoder.cc @@ -20,6 +20,7 @@ #include "draco/io/obj_encoder.h" #include "draco/io/parser_utils.h" #include "draco/io/ply_encoder.h" +#include "draco/io/stl_encoder.h" namespace { @@ -126,7 +127,6 @@ int main(int argc, char **argv) { } // Save the decoded geometry into a file. - // TODO(fgalligan): Change extension code to look for '.'. const std::string extension = draco::parser::ToLower( options.output.size() >= 4 ? options.output.substr(options.output.size() - 4) @@ -140,7 +140,7 @@ int main(int argc, char **argv) { return -1; } } else { - if (!obj_encoder.EncodeToFile(*pc.get(), options.output)) { + if (!obj_encoder.EncodeToFile(*pc, options.output)) { printf("Failed to store the decoded point cloud as OBJ.\n"); return -1; } @@ -153,13 +153,25 @@ int main(int argc, char **argv) { return -1; } } else { - if (!ply_encoder.EncodeToFile(*pc.get(), options.output)) { + if (!ply_encoder.EncodeToFile(*pc, options.output)) { printf("Failed to store the decoded point cloud as PLY.\n"); return -1; } } + } else if (extension == ".stl") { + draco::StlEncoder stl_encoder; + if (mesh) { + draco::Status s = stl_encoder.EncodeToFile(*mesh, options.output); + if (s.code() != draco::Status::OK) { + printf("Failed to store the decoded mesh as STL.\n"); + return -1; + } + } else { + printf("Can't store a point cloud as STL.\n"); + return -1; + } } else { - printf("Invalid extension of the output file. Use either .ply or .obj.\n"); + printf("Invalid output file extension. Use .obj .ply or .stl.\n"); return -1; } printf("Decoded geometry saved to %s (%" PRId64 " ms to decode)\n", diff --git a/contrib/draco/src/draco/tools/draco_encoder.cc b/contrib/draco/src/draco/tools/draco_encoder.cc index 7e3632d7d..ec639453b 100644 --- a/contrib/draco/src/draco/tools/draco_encoder.cc +++ b/contrib/draco/src/draco/tools/draco_encoder.cc @@ -15,7 +15,9 @@ #include #include +#include "draco/compression/config/compression_shared.h" #include "draco/compression/encode.h" +#include "draco/compression/expert_encode.h" #include "draco/core/cycle_timer.h" #include "draco/io/file_utils.h" #include "draco/io/mesh_io.h" @@ -35,6 +37,7 @@ struct Options { int generic_quantization_bits; bool generic_deleted; int compression_level; + bool preserve_polygons; bool use_metadata; std::string input; std::string output; @@ -50,6 +53,7 @@ Options::Options() generic_quantization_bits(8), generic_deleted(false), compression_level(7), + preserve_polygons(false), use_metadata(false) {} void Usage() { @@ -83,6 +87,11 @@ void Usage() { printf( " --metadata use metadata to encode extra information in " "mesh files.\n"); + // Mesh with polygonal faces loaded from OBJ format is converted to triangular + // mesh and polygon reconstruction information is encoded into a new generic + // attribute. + printf(" -preserve_polygons encode polygon info as an attribute.\n"); + printf( "\nUse negative quantization values to skip the specified attribute\n"); } @@ -138,12 +147,12 @@ void PrintOptions(const draco::PointCloud &pc, const Options &options) { } int EncodePointCloudToFile(const draco::PointCloud &pc, const std::string &file, - draco::Encoder *encoder) { + draco::ExpertEncoder *encoder) { draco::CycleTimer timer; // Encode the geometry. draco::EncoderBuffer buffer; timer.Start(); - const draco::Status status = encoder->EncodePointCloudToBuffer(pc, &buffer); + const draco::Status status = encoder->EncodeToBuffer(&buffer); if (!status.ok()) { printf("Failed to encode the point cloud.\n"); printf("%s\n", status.error_msg()); @@ -162,12 +171,12 @@ int EncodePointCloudToFile(const draco::PointCloud &pc, const std::string &file, } int EncodeMeshToFile(const draco::Mesh &mesh, const std::string &file, - draco::Encoder *encoder) { + draco::ExpertEncoder *encoder) { draco::CycleTimer timer; // Encode the geometry. draco::EncoderBuffer buffer; timer.Start(); - const draco::Status status = encoder->EncodeMeshToBuffer(mesh, &buffer); + const draco::Status status = encoder->EncodeToBuffer(&buffer); if (!status.ok()) { printf("Failed to encode the mesh.\n"); printf("%s\n", status.error_msg()); @@ -249,6 +258,8 @@ int main(int argc, char **argv) { ++i; } else if (!strcmp("--metadata", argv[i])) { options.use_metadata = true; + } else if (!strcmp("-preserve_polygons", argv[i])) { + options.preserve_polygons = true; } } if (argc < 3 || options.input.empty()) { @@ -259,8 +270,10 @@ int main(int argc, char **argv) { std::unique_ptr pc; draco::Mesh *mesh = nullptr; if (!options.is_point_cloud) { - auto maybe_mesh = - draco::ReadMeshFromFile(options.input, options.use_metadata); + draco::Options load_options; + load_options.SetBool("use_metadata", options.use_metadata); + load_options.SetBool("preserve_polygons", options.preserve_polygons); + auto maybe_mesh = draco::ReadMeshFromFile(options.input, load_options); if (!maybe_mesh.ok()) { printf("Failed loading the input mesh: %s.\n", maybe_mesh.status().error_msg()); @@ -350,14 +363,36 @@ int main(int argc, char **argv) { options.output = options.input + ".drc"; } - PrintOptions(*pc.get(), options); + PrintOptions(*pc, options); + + const bool input_is_mesh = mesh && mesh->num_faces() > 0; + + // Convert to ExpertEncoder that allows us to set per-attribute options. + std::unique_ptr expert_encoder; + if (input_is_mesh) { + expert_encoder.reset(new draco::ExpertEncoder(*mesh)); + } else { + expert_encoder.reset(new draco::ExpertEncoder(*pc)); + } + expert_encoder->Reset(encoder.CreateExpertEncoderOptions(*pc)); + + // Check if there is an attribute that stores polygon edges. If so, we disable + // the default prediction scheme for the attribute as it actually makes the + // compression worse. + const int poly_att_id = + pc->GetAttributeIdByMetadataEntry("name", "added_edges"); + if (poly_att_id != -1) { + expert_encoder->SetAttributePredictionScheme( + poly_att_id, draco::PredictionSchemeMethod::PREDICTION_NONE); + } int ret = -1; - const bool input_is_mesh = mesh && mesh->num_faces() > 0; - if (input_is_mesh) - ret = EncodeMeshToFile(*mesh, options.output, &encoder); - else - ret = EncodePointCloudToFile(*pc.get(), options.output, &encoder); + + if (input_is_mesh) { + ret = EncodeMeshToFile(*mesh, options.output, expert_encoder.get()); + } else { + ret = EncodePointCloudToFile(*pc, options.output, expert_encoder.get()); + } if (ret != -1 && options.compression_level < 10) { printf( diff --git a/contrib/draco/src/draco/tools/draco_transcoder.cc b/contrib/draco/src/draco/tools/draco_transcoder.cc new file mode 100644 index 000000000..ad0f27714 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder.cc @@ -0,0 +1,130 @@ +// Copyright 2019 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include +#include + +#include "draco/core/cycle_timer.h" +#include "draco/core/status.h" +#include "draco/draco_features.h" +#include "draco/texture/texture_utils.h" +#include "draco/tools/draco_transcoder_lib.h" + +namespace { + +// TODO(fgalligan): Add support for no compression to the transcoder lib. +void Usage() { + // TODO(b/204212351): Revisit using a raw string literal here for readability. + printf("Usage: draco_transcoder [options] -i input -o output\n\n"); + printf("Main options:\n"); + printf(" -h | -? show help.\n"); + printf(" -i input file name.\n"); + printf(" -o output file name.\n"); + printf(" -qp quantization bits for the position attribute, "); + printf("default=11.\n"); + printf(" -qt quantization bits for the texture coordinate "); + printf("attribute, default=10.\n"); + printf(" -qn quantization bits for the normal vector attribute"); + printf(", default=8.\n"); + printf(" -qc quantization bits for the color attribute, "); + printf("default=8.\n"); + printf(" -qtg quantization bits for the tangent attribute, "); + printf("default=8.\n"); + printf(" -qw quantization bits for the weight attribute, "); + printf("default=8.\n"); + printf(" -qg quantization bits for any generic attribute, "); + printf("default=8.\n"); + + printf("\nBoolean options may be negated by prefixing 'no'.\n"); +} + +int StringToInt(const std::string &s) { + char *end; + return strtol(s.c_str(), &end, 10); // NOLINT +} + +bool MatchesBooleanOption(const std::string &option, const std::string &value) { + const std::string opt = "-" + option; + const std::string noopt = "-no" + option; + return value == opt || value == noopt; +} + +draco::Status TranscodeFile( + const draco::DracoTranscoder::FileOptions &file_options, + const draco::DracoTranscodingOptions &transcode_options) { + draco::CycleTimer timer; + timer.Start(); + DRACO_ASSIGN_OR_RETURN(std::unique_ptr dt, + draco::DracoTranscoder::Create(transcode_options)); + + DRACO_RETURN_IF_ERROR(dt->Transcode(file_options)); + timer.Stop(); + printf("Transcode\t%s\t%" PRId64 "\n", file_options.input_filename.c_str(), + timer.GetInMs()); + + return draco::OkStatus(); +} + +} // anonymous namespace + +int main(int argc, char **argv) { + draco::DracoTranscoder::FileOptions file_options; + draco::DracoTranscodingOptions transcode_options; + const int argc_check = argc - 1; + + for (int i = 1; i < argc; ++i) { + if (!strcmp("-h", argv[i]) || !strcmp("-?", argv[i])) { + Usage(); + return 0; + } else if (!strcmp("-i", argv[i]) && i < argc_check) { + file_options.input_filename = argv[++i]; + } else if (!strcmp("-o", argv[i]) && i < argc_check) { + file_options.output_filename = argv[++i]; + } else if (!strcmp("-qp", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_position.SetQuantizationBits( + StringToInt(argv[++i])); + } else if (!strcmp("-qt", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_tex_coord = + StringToInt(argv[++i]); + } else if (!strcmp("-qn", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_normal = + StringToInt(argv[++i]); + } else if (!strcmp("-qc", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_color = + StringToInt(argv[++i]); + } else if (!strcmp("-qtg", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_tangent = + StringToInt(argv[++i]); + } else if (!strcmp("-qw", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_weight = + StringToInt(argv[++i]); + } else if (!strcmp("-qg", argv[i]) && i < argc_check) { + transcode_options.geometry.quantization_bits_generic = + StringToInt(argv[++i]); + } + } + if (argc < 3 || file_options.input_filename.empty() || + file_options.output_filename.empty()) { + Usage(); + return -1; + } + + const draco::Status status = TranscodeFile(file_options, transcode_options); + if (!status.ok()) { + printf("Failed\t%s\t%s\n", file_options.input_filename.c_str(), + status.error_msg()); + return -1; + } + return 0; +} diff --git a/contrib/draco/src/draco/tools/draco_transcoder_lib.cc b/contrib/draco/src/draco/tools/draco_transcoder_lib.cc new file mode 100644 index 000000000..b0bc43843 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder_lib.cc @@ -0,0 +1,86 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#include "draco/tools/draco_transcoder_lib.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include "draco/core/status_or.h" +#include "draco/io/file_utils.h" +#include "draco/io/scene_io.h" +#include "draco/scene/scene_utils.h" +#include "draco/texture/texture_utils.h" + +namespace draco { + +DracoTranscoder::DracoTranscoder() {} + +StatusOr> DracoTranscoder::Create( + const DracoTranscodingOptions &options) { + DRACO_RETURN_IF_ERROR(options.geometry.Check()); + std::unique_ptr dt(new DracoTranscoder()); + dt->transcoding_options_ = options; + return dt; +} + +StatusOr> DracoTranscoder::Create( + const DracoCompressionOptions &options) { + DracoTranscodingOptions new_options; + new_options.geometry = options; + return Create(new_options); +} + +Status DracoTranscoder::Transcode(const FileOptions &file_options) { + DRACO_RETURN_IF_ERROR(ReadScene(file_options)); + DRACO_RETURN_IF_ERROR(CompressScene()); + DRACO_RETURN_IF_ERROR(WriteScene(file_options)); + return OkStatus(); +} + +Status DracoTranscoder::ReadScene(const FileOptions &file_options) { + if (file_options.input_filename.empty()) { + return Status(Status::DRACO_ERROR, "Input filename is empty."); + } else if (file_options.output_filename.empty()) { + return Status(Status::DRACO_ERROR, "Output filename is empty."); + } + DRACO_ASSIGN_OR_RETURN(scene_, + ReadSceneFromFile(file_options.input_filename)); + return OkStatus(); +} + +Status DracoTranscoder::WriteScene(const FileOptions &file_options) { + if (!file_options.output_bin_filename.empty() && + !file_options.output_resource_directory.empty()) { + DRACO_RETURN_IF_ERROR(gltf_encoder_.EncodeFile( + *scene_, file_options.output_filename, file_options.output_bin_filename, + file_options.output_resource_directory)); + } else if (!file_options.output_bin_filename.empty()) { + DRACO_RETURN_IF_ERROR( + gltf_encoder_.EncodeFile(*scene_, file_options.output_filename, + file_options.output_bin_filename)); + } else { + DRACO_RETURN_IF_ERROR( + gltf_encoder_.EncodeFile(*scene_, file_options.output_filename)); + } + return OkStatus(); +} + +Status DracoTranscoder::CompressScene() { + // Apply geometry compression settings to all scene meshes. + SceneUtils::SetDracoCompressionOptions(&transcoding_options_.geometry, + scene_.get()); + return OkStatus(); +} + +} // namespace draco +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/tools/draco_transcoder_lib.h b/contrib/draco/src/draco/tools/draco_transcoder_lib.h new file mode 100644 index 000000000..c94d19de7 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder_lib.h @@ -0,0 +1,103 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifndef DRACO_TOOLS_DRACO_TRANSCODER_LIB_H_ +#define DRACO_TOOLS_DRACO_TRANSCODER_LIB_H_ + +#include "draco/draco_features.h" + +#ifdef DRACO_TRANSCODER_SUPPORTED +#include + +#include "draco/compression/draco_compression_options.h" +#include "draco/core/options.h" +#include "draco/io/gltf_encoder.h" +#include "draco/io/image_compression_options.h" + +namespace draco { + +// Struct to hold Draco transcoding options. +struct DracoTranscodingOptions { + DracoTranscodingOptions() {} + + // Options used when geometry compression optimization is disabled. + DracoCompressionOptions geometry; +}; + +// Class that supports input of glTF (and some simple USD) files, encodes +// them with Draco compression, and outputs glTF Draco compressed files. +// +// glTF supported extensions: +// Input and Output: +// KHR_draco_mesh_compression. http://shortn/_L5tPQqdwWf +// KHR_materials_unlit. http://shortn/_3eaDLoIGam +// KHR_texture_transform. http://shortn/_PORWgVTEe8 +// +// glTF unsupported features: +// Input and Output: +// Morph targets. http://shortn/_zE5DLw8a9B +// Sparse accessors. http://shortn/_h3FwbzQl4f +// KHR_lights_punctual. http://shortn/_nzGk80wKtK +// KHR_materials_pbrSpecularGlossiness. http://shortn/_iz0VC6dIKe +// All vendor extensions. +class DracoTranscoder { + public: + struct FileOptions { + std::string input_filename; // Must be non-empty. + std::string output_filename; // Must be non-empty. + std::string output_bin_filename = ""; + std::string output_resource_directory = ""; + }; + + DracoTranscoder(); + + // Creates a DracoTranscoder object. |options| sets the compression options + // used in the Encode function. + static StatusOr> Create( + const DracoTranscodingOptions &options); + + // Deprecated. + // TODO(fgalligan): Remove when function is not being used anymore. + static StatusOr> Create( + const DracoCompressionOptions &options); + + // Encodes the input with Draco compression using the compression options + // passed in the Create function. The recommended use case is to create a + // transcoder once and call Transcode for multiple files. + Status Transcode(const FileOptions &file_options); + + private: + // Read scene from file. + Status ReadScene(const FileOptions &file_options); + + // Write scene to file. + Status WriteScene(const FileOptions &file_options); + + // Apply compression settings to the scene. + Status CompressScene(); + + private: + GltfEncoder gltf_encoder_; + + // The scene being transcoded. + std::unique_ptr scene_; + + // Copy of the transcoding options passed into the Create function. + DracoTranscodingOptions transcoding_options_; +}; + +} // namespace draco + +#endif // DRACO_TRANSCODER_SUPPORTED +#endif // DRACO_TOOLS_DRACO_TRANSCODER_LIB_H_ diff --git a/contrib/draco/src/draco/tools/draco_transcoder_lib_test.cc b/contrib/draco/src/draco/tools/draco_transcoder_lib_test.cc new file mode 100644 index 000000000..a87a158e0 --- /dev/null +++ b/contrib/draco/src/draco/tools/draco_transcoder_lib_test.cc @@ -0,0 +1,172 @@ +// Copyright 2021 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#ifdef DRACO_TRANSCODER_SUPPORTED + +#include "draco/tools/draco_transcoder_lib.h" + +#include "draco/core/draco_test_base.h" +#include "draco/core/draco_test_utils.h" +#include "draco/io/file_utils.h" + +// Tests encoding a .gltf file with default Draco compression. +TEST(DracoTranscoderTest, DefaultDracoCompression) { + const std::string input_name = "sphere.gltf"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("test.bin"); + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); +} + +// Tests setting the output glTF .bin name. +TEST(DracoTranscoderTest, TestBinName) { + const std::string input_name = "sphere.gltf"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("different_name.bin"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + file_options.output_bin_filename = output_bin_filename; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); +} + +// Tests setting the output glTF resource directory. +TEST(DracoTranscoderTest, TestResourceDirName) { + const std::string input_name = "sphere.gltf"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("another_name.bin"); + const std::string output_resource_directory = + draco::GetTestTempFileFullPath("res/other_files"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + file_options.output_bin_filename = output_bin_filename; + file_options.output_resource_directory = output_resource_directory; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); + + const std::string res_dir_png_filename = draco::GetTestTempFileFullPath( + "res/other_files/sphere_Texture0_Normal.png"); + const size_t output_png_size = draco::GetFileSize(res_dir_png_filename); + ASSERT_GT(output_png_size, 0); +} + +// Tests creating one transcoder to encode multiple files. +TEST(DracoTranscoderTest, EncodeMultipleFiles) { + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = draco::GetTestFileFullPath("sphere.gltf"); + file_options.output_filename = draco::GetTestTempFileFullPath("first.gltf"); + DRACO_ASSERT_OK(dt->Transcode(file_options)); + const size_t first_bin_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("first.bin")); + ASSERT_GT(first_bin_size, 0); + + file_options.input_filename = + draco::GetTestFileFullPath("CesiumMan/glTF/CesiumMan.gltf"); + file_options.output_filename = draco::GetTestTempFileFullPath("second.gltf"); + DRACO_ASSERT_OK(dt->Transcode(file_options)); + const size_t second_bin_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("second.bin")); + ASSERT_GT(second_bin_size, 0); +} + +// Tests using glTF binary as input. +TEST(DracoTranscoderTest, SimpleGlbInput) { + const std::string input_name = "Box/glTF_Binary/Box.glb"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + const std::string output_filename = + draco::GetTestTempFileFullPath("test.gltf"); + + const draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = output_filename; + DRACO_ASSERT_OK(dt->Transcode(file_options)); + + const std::string output_bin_filename = + draco::GetTestTempFileFullPath("test.bin"); + const size_t output_bin_size = draco::GetFileSize(output_bin_filename); + ASSERT_GT(output_bin_size, 0); +} + +// Simple test to check glb input and setting smaller position quantizations +// outputs a smaller file overall. +TEST(DracoTranscoderTest, TestPositionQuantization) { + const std::string input_name = + "KhronosSampleModels/Duck/glTF_Binary/Duck.glb"; + const std::string input_filename = draco::GetTestFileFullPath(input_name); + + draco::DracoTranscodingOptions options; + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt, + draco::DracoTranscoder::Create(options)); + + draco::DracoTranscoder::FileOptions file_options; + file_options.input_filename = input_filename; + file_options.output_filename = draco::GetTestTempFileFullPath("first.glb"); + DRACO_ASSERT_OK(dt->Transcode(file_options)); + const size_t first_glb_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("first.glb")); + + options.geometry.quantization_position.SetQuantizationBits(10); + DRACO_ASSIGN_OR_ASSERT(std::unique_ptr dt2, + draco::DracoTranscoder::Create(options)); + file_options.output_filename = draco::GetTestTempFileFullPath("second.glb"); + DRACO_ASSERT_OK(dt2->Transcode(file_options)); + const size_t second_glb_size = + draco::GetFileSize(draco::GetTestTempFileFullPath("second.glb")); + ASSERT_GT(first_glb_size, second_glb_size); +} + +#endif // DRACO_TRANSCODER_SUPPORTED diff --git a/contrib/draco/src/draco/tools/fuzz/build.sh b/contrib/draco/src/draco/tools/fuzz/build.sh index bbeb10591..3e48409fc 100644 --- a/contrib/draco/src/draco/tools/fuzz/build.sh +++ b/contrib/draco/src/draco/tools/fuzz/build.sh @@ -19,7 +19,7 @@ cmake $SRC/draco # The draco_decoder and draco_encoder binaries don't build nicely with OSS-Fuzz # options, so just build the Draco shared libraries. -make -j$(nproc) draco +make -j$(nproc) # build fuzzers for fuzzer in $(find $SRC/draco/src/draco/tools/fuzz -name '*.cc'); do diff --git a/contrib/draco/src/draco/tools/install_test/CMakeLists.txt b/contrib/draco/src/draco/tools/install_test/CMakeLists.txt new file mode 100644 index 000000000..800dc9c9b --- /dev/null +++ b/contrib/draco/src/draco/tools/install_test/CMakeLists.txt @@ -0,0 +1,25 @@ +# Copyright 2022 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +cmake_minimum_required(VERSION 3.12) +project(install_test C CXX) + +include(GNUInstallDirs) + +find_package(draco REQUIRED CONFIG) + +add_executable(install_check main.cc) +target_link_libraries(install_check PRIVATE draco::draco) + +install(TARGETS install_check DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/contrib/draco/src/draco/tools/install_test/main.cc b/contrib/draco/src/draco/tools/install_test/main.cc new file mode 100644 index 000000000..e76793b64 --- /dev/null +++ b/contrib/draco/src/draco/tools/install_test/main.cc @@ -0,0 +1,44 @@ +// Copyright 2022 The Draco Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// This program is used to test the installed version of Draco. It does just +// enough to confirm that an application using Draco can compile and link +// against an installed version of Draco without errors. It does not perform +// any sort of library tests. + +#include +#include + +#include "draco/core/decoder_buffer.h" + +#if defined DRACO_TRANSCODER_SUPPORTED +#include "draco/scene/scene.h" +#include "draco/scene/scene_utils.h" +#endif + +int main(int /*argc*/, char** /*argv*/) { + std::vector empty_buffer; + draco::DecoderBuffer buffer; + buffer.Init(empty_buffer.data(), empty_buffer.size()); + +#if defined DRACO_TRANSCODER_SUPPORTED + draco::Scene empty_scene; + const int num_meshes = empty_scene.NumMeshes(); + (void)num_meshes; +#endif + + printf("Partial sanity test passed.\n"); + return 0; +} diff --git a/contrib/draco/src/draco/tools/install_test/test.py b/contrib/draco/src/draco/tools/install_test/test.py new file mode 100644 index 000000000..8612e70b5 --- /dev/null +++ b/contrib/draco/src/draco/tools/install_test/test.py @@ -0,0 +1,456 @@ +#!/usr/bin/python3 +# +# Copyright 2022 The Draco Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests installations of the Draco library. + +Builds the library in shared and static configurations on the current host +system, and then confirms that a simple test application can link in both +configurations. +""" + +import argparse +import multiprocessing +import os +import pathlib +import shlex +import shutil +import subprocess +import sys + +# CMake executable. +CMAKE = shutil.which('cmake') + +# List of generators available in the current CMake executable. +CMAKE_AVAILABLE_GENERATORS = [] + +# List of variable defs to be passed through to CMake via its -D argument. +CMAKE_DEFINES = [] + +# CMake builds use the specified generator. +CMAKE_GENERATOR = None + +# Enable the transcoder before running tests (sets DRACO_TRANSCODER_SUPPORTED +# and builds transcoder support dependencies). +ENABLE_TRANSCODER = False + +# The Draco tree that this script uses. +DRACO_SOURCES_PATH = os.path.abspath(os.path.join('..', '..', '..', '..')) + +# Path to this script and the rest of the test project files. +TEST_SOURCES_PATH = os.path.dirname(os.path.abspath(__file__)) + +# The Draco build directories. +DRACO_SHARED_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_draco_build_shared') +DRACO_STATIC_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_draco_build_static') + +# The Draco install roots. +DRACO_SHARED_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_draco_install_shared') +DRACO_STATIC_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_draco_install_static') + +DRACO_SHARED_INSTALL_BIN_PATH = os.path.join(DRACO_SHARED_INSTALL_PATH, 'bin') + +DRACO_SHARED_INSTALL_LIB_PATH = os.path.join(DRACO_SHARED_INSTALL_PATH, 'lib') + +# Argument for -j when using make, or -m when using Visual Studio. Number of +# build jobs. +NUM_PROCESSES = multiprocessing.cpu_count() - 1 + +# The test project build directories. +TEST_SHARED_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_test_build_shared') +TEST_STATIC_BUILD_PATH = os.path.join(TEST_SOURCES_PATH, '_test_build_static') + +# The test project install directories. +TEST_SHARED_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_test_install_shared') +TEST_STATIC_INSTALL_PATH = os.path.join(TEST_SOURCES_PATH, + '_test_install_static') + +# Show configuration and build output. +VERBOSE = False + + +def cmake_get_available_generators(): + """Returns list of generators available in current CMake executable.""" + result = run_process_and_capture_output(f'{CMAKE} --help') + + if result[0] != 0: + raise Exception(f'cmake --help failed, exit code: {result[0]}\n{result[1]}') + + help_text = result[1].splitlines() + generators_start_index = help_text.index('Generators') + 3 + generators_text = help_text[generators_start_index::] + + generators = [] + for gen in generators_text: + gen = gen.split('=')[0].strip().replace('* ', '') + + if gen and gen[0] != '=': + generators.append(gen) + + return generators + + +def cmake_get_generator(): + """Returns the CMake generator from CMakeCache.txt in the current dir.""" + cmake_cache_file_path = os.path.join(os.getcwd(), 'CMakeCache.txt') + cmake_cache_text = '' + with open(cmake_cache_file_path, 'r') as cmake_cache_file: + cmake_cache_text = cmake_cache_file.read() + + if not cmake_cache_text: + raise FileNotFoundError(f'{cmake_cache_file_path} missing or empty.') + + generator = '' + for line in cmake_cache_text.splitlines(): + if line.startswith('CMAKE_GENERATOR:INTERNAL='): + generator = line.split('=')[1] + + return generator + + +def run_process_and_capture_output(cmd, env=None): + """Runs |cmd| as a child process. + + Returns process exit code and output. Streams process output to stdout when + VERBOSE is true. + + Args: + cmd: String containing the command to execute. + env: Optional dict of environment variables. + + Returns: + Tuple of exit code and output. + """ + if not cmd: + raise ValueError('run_process_and_capture_output requires cmd argument.') + + if os.name == 'posix': + # On posix systems subprocess.Popen will treat |cmd| as the program name + # when it is passed as a string. Unconditionally split the command so + # callers don't need to care about this detail. + cmd = shlex.split(cmd) + + proc = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + + if VERBOSE: + print('COMMAND output:') + + stdout = '' + for line in iter(proc.stdout.readline, b''): + decoded_line = line.decode('utf-8') + if VERBOSE: + sys.stdout.write(decoded_line) + sys.stdout.flush() + stdout += decoded_line + + # Wait for the process to exit so that the exit code is available. + proc.wait() + return [proc.returncode, stdout] + + +def create_output_directories(): + """Creates the build output directores for the test.""" + pathlib.Path(DRACO_SHARED_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(DRACO_STATIC_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(TEST_SHARED_BUILD_PATH).mkdir(parents=True, exist_ok=True) + pathlib.Path(TEST_STATIC_BUILD_PATH).mkdir(parents=True, exist_ok=True) + + +def cleanup(): + """Removes the build output directories from the test.""" + shutil.rmtree(DRACO_SHARED_BUILD_PATH) + shutil.rmtree(DRACO_STATIC_BUILD_PATH) + shutil.rmtree(DRACO_SHARED_INSTALL_PATH) + shutil.rmtree(DRACO_STATIC_INSTALL_PATH) + shutil.rmtree(TEST_SHARED_BUILD_PATH) + shutil.rmtree(TEST_STATIC_BUILD_PATH) + shutil.rmtree(TEST_SHARED_INSTALL_PATH) + shutil.rmtree(TEST_STATIC_INSTALL_PATH) + + +def cmake_configure(source_path, cmake_args=None): + """Configures a CMake build.""" + command = f'{CMAKE} {source_path}' + + if CMAKE_GENERATOR: + if ' ' in CMAKE_GENERATOR: + command += f' -G "{CMAKE_GENERATOR}"' + else: + command += f' -G {CMAKE_GENERATOR}' + + if cmake_args: + for arg in cmake_args: + command += f' {arg}' + + if CMAKE_DEFINES: + for arg in CMAKE_DEFINES: + command += f' -D{arg}' + + if VERBOSE: + print(f'CONFIGURE command:\n{command}') + + result = run_process_and_capture_output(command) + + if result[0] != 0: + raise Exception(f'CONFIGURE failed!\nexit_code: {result[0]}\n{result[1]}') + + +def cmake_build(cmake_args=None, build_args=None): + """Runs a CMake build.""" + command = f'{CMAKE} --build .' + + if cmake_args: + for arg in cmake_args: + command += f' {arg}' + + if not build_args: + build_args = [] + + generator = cmake_get_generator() + if generator.endswith('Makefiles'): + build_args.append(f'-j {NUM_PROCESSES}') + elif generator.startswith('Visual'): + build_args.append(f'-m:{NUM_PROCESSES}') + + if build_args: + command += ' --' + for arg in build_args: + command += f' {arg}' + + if VERBOSE: + print(f'BUILD command:\n{command}') + + result = run_process_and_capture_output(f'{command}') + + if result[0] != 0: + raise Exception(f'BUILD failed!\nexit_code: {result[0]}\n{result[1]}') + + +def run_install_check(install_path): + """Runs the install_check program.""" + cmd = os.path.join(install_path, 'bin', 'install_check') + if VERBOSE: + print(f'RUN command: {cmd}') + + result = run_process_and_capture_output( + cmd, + # On Windows, add location of draco.dll into PATH env var + {'PATH': DRACO_SHARED_INSTALL_BIN_PATH + os.pathsep + os.environ['PATH']}, + ) + if result[0] != 0: + raise Exception( + f'install_check run failed!\nexit_code: {result[0]}\n{result[1]}') + + +def build_and_install_transcoder_dependencies(): + """Builds and installs Draco dependencies for transcoder enabled builds.""" + orig_dir = os.getcwd() + + # The Eigen CMake build in the release Draco has pinned is, to put it mildly, + # user unfriendly. Instead of wasting time trying to integrate it here, just + # shutil.copytree() everything in $eigen_submodule_path to + # $CMAKE_INSTALL_PREFIX/include/Eigen. + # Eigen claims to be header-only, so this should be adequate for Draco's + # needs here. + eigen_submodule_path = os.path.join( + DRACO_SOURCES_PATH, 'third_party', 'eigen', 'Eigen') + + # "Install" Eigen for the shared install root. + eigen_install_path = os.path.join( + DRACO_SHARED_INSTALL_PATH, 'include', 'Eigen') + shutil.copytree(src=eigen_submodule_path, dst=eigen_install_path) + + # "Install" Eigen for the static install root. + eigen_install_path = os.path.join( + DRACO_STATIC_INSTALL_PATH, 'include', 'Eigen') + shutil.copytree(src=eigen_submodule_path, dst=eigen_install_path) + + # Build and install gulrak/filesystem for shared and static configurations. + # Note that this is basically running gulrak/filesystem's CMake build as an + # install script. + fs_submodule_path = os.path.join( + DRACO_SOURCES_PATH, 'third_party', 'filesystem') + + # Install gulrak/filesystem in the shared draco install root. + fs_shared_build = os.path.join(DRACO_SHARED_BUILD_PATH, '_fs') + pathlib.Path(fs_shared_build).mkdir(parents=True, exist_ok=True) + os.chdir(fs_shared_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=ON') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_TESTING=OFF') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=fs_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Install gulrak/filesystem in the shared draco install root. + fs_static_build = os.path.join(DRACO_STATIC_BUILD_PATH, '_fs') + pathlib.Path(fs_static_build).mkdir(parents=True, exist_ok=True) + os.chdir(fs_static_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=OFF') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_TESTING=OFF') + cmake_args.append('-DGHC_FILESYSTEM_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=fs_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Build and install TinyGLTF for shared and static configurations. + # Note, as above, that this is basically running TinyGLTF's CMake build as an + # install script. + tinygltf_submodule_path = os.path.join( + DRACO_SOURCES_PATH, 'third_party', 'tinygltf') + + # Install TinyGLTF in the shared draco install root. + tinygltf_shared_build = os.path.join(DRACO_SHARED_BUILD_PATH, '_TinyGLTF') + pathlib.Path(tinygltf_shared_build).mkdir(parents=True, exist_ok=True) + os.chdir(tinygltf_shared_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DTINYGLTF_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=tinygltf_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Install TinyGLTF in the static draco install root. + tinygltf_static_build = os.path.join(DRACO_STATIC_BUILD_PATH, '_TinyGLTF') + pathlib.Path(tinygltf_static_build).mkdir(parents=True, exist_ok=True) + os.chdir(tinygltf_static_build) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_STATIC_INSTALL_PATH}') + cmake_args.append('-DTINYGLTF_BUILD_EXAMPLES=OFF') + cmake_configure(source_path=tinygltf_submodule_path, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + os.chdir(orig_dir) + + +def build_and_install_draco(): + """Builds Draco in shared and static configurations.""" + orig_dir = os.getcwd() + + if ENABLE_TRANSCODER: + build_and_install_transcoder_dependencies() + + # Build and install Draco in shared library config for the current host + # machine. + os.chdir(DRACO_SHARED_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=ON') + if ENABLE_TRANSCODER: + cmake_args.append('-DDRACO_TRANSCODER_SUPPORTED=ON') + cmake_configure(source_path=DRACO_SOURCES_PATH, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + # Build and install Draco in the static config for the current host machine. + os.chdir(DRACO_STATIC_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={DRACO_STATIC_INSTALL_PATH}') + cmake_args.append('-DBUILD_SHARED_LIBS=OFF') + if ENABLE_TRANSCODER: + cmake_args.append('-DDRACO_TRANSCODER_SUPPORTED=ON') + cmake_configure(source_path=DRACO_SOURCES_PATH, cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + + os.chdir(orig_dir) + + +def build_test_project(): + """Builds the test application in shared and static configurations.""" + orig_dir = os.getcwd() + + # Configure the test project against draco shared and build it. + os.chdir(TEST_SHARED_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={TEST_SHARED_INSTALL_PATH}') + cmake_args.append(f'-DCMAKE_PREFIX_PATH={DRACO_SHARED_INSTALL_PATH}') + cmake_args.append(f'-DCMAKE_INSTALL_RPATH={DRACO_SHARED_INSTALL_LIB_PATH}') + cmake_configure(source_path=f'{TEST_SOURCES_PATH}', cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + run_install_check(TEST_SHARED_INSTALL_PATH) + + # Configure the test project against draco static and build it. + os.chdir(TEST_STATIC_BUILD_PATH) + cmake_args = [] + cmake_args.append(f'-DCMAKE_INSTALL_PREFIX={TEST_STATIC_INSTALL_PATH}') + cmake_args.append(f'-DCMAKE_PREFIX_PATH={DRACO_STATIC_INSTALL_PATH}') + cmake_configure(source_path=f'{TEST_SOURCES_PATH}', cmake_args=cmake_args) + cmake_build(cmake_args=['--target install']) + run_install_check(TEST_STATIC_INSTALL_PATH) + + os.chdir(orig_dir) + + +def test_draco_install(): + create_output_directories() + build_and_install_draco() + build_test_project() + cleanup() + + +if __name__ == '__main__': + CMAKE_AVAILABLE_GENERATORS = cmake_get_available_generators() + + parser = argparse.ArgumentParser() + parser.add_argument( + '-G', '--generator', help='CMake builds use the specified generator.') + parser.add_argument( + '-D', '--cmake_define', + action='append', + help='Passes argument through to CMake as a CMake variable via cmake -D.') + parser.add_argument( + '-t', '--with_transcoder', + action='store_true', + help='Run tests with Draco transcoder support enabled.') + parser.add_argument( + '-v', + '--verbose', + action='store_true', + help='Show configuration and build output.') + args = parser.parse_args() + + if args.cmake_define: + CMAKE_DEFINES = args.cmake_define + if args.generator: + CMAKE_GENERATOR = args.generator + if args.verbose: + VERBOSE = True + if args.with_transcoder: + ENABLE_TRANSCODER = True + + if VERBOSE: + print(f'CMAKE={CMAKE}') + print(f'CMAKE_DEFINES={CMAKE_DEFINES}') + print(f'CMAKE_GENERATOR={CMAKE_GENERATOR}') + print(f'CMAKE_AVAILABLE_GENERATORS={CMAKE_AVAILABLE_GENERATORS}') + print(f'ENABLE_TRANSCODER={ENABLE_TRANSCODER}') + print(f'DRACO_SOURCES_PATH={DRACO_SOURCES_PATH}') + print(f'DRACO_SHARED_BUILD_PATH={DRACO_SHARED_BUILD_PATH}') + print(f'DRACO_STATIC_BUILD_PATH={DRACO_STATIC_BUILD_PATH}') + print(f'DRACO_SHARED_INSTALL_PATH={DRACO_SHARED_INSTALL_PATH}') + print(f'DRACO_STATIC_INSTALL_PATH={DRACO_STATIC_INSTALL_PATH}') + print(f'NUM_PROCESSES={NUM_PROCESSES}') + print(f'TEST_SHARED_BUILD_PATH={TEST_SHARED_BUILD_PATH}') + print(f'TEST_STATIC_BUILD_PATH={TEST_STATIC_BUILD_PATH}') + print(f'TEST_SOURCES_PATH={TEST_SOURCES_PATH}') + print(f'VERBOSE={VERBOSE}') + + if CMAKE_GENERATOR and CMAKE_GENERATOR not in CMAKE_AVAILABLE_GENERATORS: + raise ValueError(f'CMake generator unavailable: {CMAKE_GENERATOR}.') + + test_draco_install()