mirror of
https://git.mirrors.martin98.com/https://github.com/google/draco
synced 2025-09-15 17:43:15 +08:00
Updated snapshot to 1.3.2
- Bug fixes
This commit is contained in:
parent
6d70ba7033
commit
79774fec74
@ -548,6 +548,7 @@ set(draco_js_enc_sources
|
||||
"${draco_src_root}/javascript/emscripten/draco_encoder_glue_wrapper.cc"
|
||||
"${draco_src_root}/javascript/emscripten/encoder_webidl_wrapper.cc")
|
||||
|
||||
|
||||
set(draco_test_sources
|
||||
"${draco_src_root}/attributes/point_attribute_test.cc"
|
||||
"${draco_src_root}/compression/attributes/prediction_schemes/prediction_scheme_normal_octahedron_canonicalized_transform_test.cc"
|
||||
@ -648,6 +649,7 @@ if (EMSCRIPTEN AND ENABLE_JS_GLUE)
|
||||
message(FATAL_ERROR "Glue generation failed.")
|
||||
endif ()
|
||||
|
||||
|
||||
# Add a custom rule depending on the IDL to regenerate
|
||||
# ${draco_build_dir}/glue_decoder.cpp as needed.
|
||||
add_custom_command(OUTPUT ${draco_build_dir}/glue_decoder.cpp
|
||||
@ -670,6 +672,7 @@ if (EMSCRIPTEN AND ENABLE_JS_GLUE)
|
||||
WORKING_DIRECTORY ${draco_build_dir}
|
||||
VERBATIM)
|
||||
|
||||
|
||||
set(draco_decoder_src
|
||||
${draco_attributes_sources}
|
||||
${draco_compression_attributes_dec_sources}
|
||||
@ -687,7 +690,6 @@ if (EMSCRIPTEN AND ENABLE_JS_GLUE)
|
||||
${draco_point_cloud_sources}
|
||||
${draco_points_dec_sources}
|
||||
${draco_version_sources})
|
||||
|
||||
set(draco_encoder_src
|
||||
${draco_attributes_sources}
|
||||
${draco_compression_attributes_enc_sources}
|
||||
@ -792,7 +794,7 @@ else ()
|
||||
add_library(draco_unity_plugin OBJECT
|
||||
${draco_unity_plug_sources})
|
||||
add_library(draco_maya_plugin OBJECT
|
||||
${draco_maya_plug_sources})
|
||||
${draco_maya_plug_sources})
|
||||
|
||||
# Library targets that consume the object collections.
|
||||
add_library(dracodec
|
||||
@ -904,13 +906,13 @@ else ()
|
||||
$<TARGET_OBJECTS:draco_points_dec>
|
||||
$<TARGET_OBJECTS:draco_metadata_enc>
|
||||
$<TARGET_OBJECTS:draco_points_enc>)
|
||||
|
||||
|
||||
# For Mac, we need to build a .bundle for plugin.
|
||||
if (APPLE)
|
||||
set_target_properties(draco_maya_wrapper PROPERTIES BUNDLE true)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
|
||||
set(draco_header_only_targets
|
||||
draco_compression_attributes_pred_schemes_dec
|
||||
draco_dec_config
|
||||
|
@ -5,6 +5,9 @@
|
||||
|
||||
News
|
||||
=======
|
||||
### Version 1.3.2 release
|
||||
* Bug fixes
|
||||
|
||||
### Version 1.3.1 release
|
||||
* Fix issue with multiple attributes when skipping an attribute transform
|
||||
|
||||
|
@ -8,5 +8,6 @@
|
||||
// GENERATED FILE, DO NOT EDIT. SEE ABOVE.
|
||||
|
||||
#define DRACO_TEST_DATA_DIR "${DRACO_TEST_DATA_DIR}"
|
||||
#define DRACO_TEST_TEMP_DIR "${DRACO_TEST_TEMP_DIR}"
|
||||
|
||||
#endif // DRACO_TESTING_DRACO_TEST_CONFIG_H_
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -5,6 +5,9 @@
|
||||
|
||||
News
|
||||
=======
|
||||
### Version 1.3.2 release
|
||||
* Bug fixes
|
||||
|
||||
### Version 1.3.1 release
|
||||
* Fix issue with multiple attributes when skipping an attribute transform
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "draco3d",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"description": "Draco is a library for compressing and decompressing 3D geometric meshes and point clouds. It is intended to improve the storage and transmission of 3D graphics.",
|
||||
"main": "draco3d.js",
|
||||
"scripts": {
|
||||
|
@ -16,6 +16,9 @@ TODO: Add glTF branch url.
|
||||
|
||||
News
|
||||
=======
|
||||
### Version 1.3.2 release
|
||||
* Bug Fixes
|
||||
|
||||
### Version 1.3.1 release
|
||||
* Fix issue with multiple attributes when skipping an attribute transform
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "draco3dgltf",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"description": "This package contains a specific version of Draco 3D geometric compression library that is used for glTF Draco mesh compression extension.",
|
||||
"main": "draco3dgltf.js",
|
||||
"scripts": {
|
||||
|
@ -46,6 +46,9 @@ static constexpr CornerIndex kInvalidCornerIndex(
|
||||
static constexpr FaceIndex kInvalidFaceIndex(
|
||||
std::numeric_limits<uint32_t>::max());
|
||||
|
||||
// TODO(ostava): Add strongly typed indices for attribute id and unique
|
||||
// attribute id.
|
||||
|
||||
} // namespace draco
|
||||
|
||||
#endif // DRACO_ATTRIBUTES_GEOMETRY_INDICES_H_
|
||||
|
@ -72,7 +72,7 @@ bool AttributesDecoder::DecodeAttributesDecoderData(DecoderBuffer *in_buffer) {
|
||||
uint16_t custom_id;
|
||||
if (!in_buffer->Decode(&custom_id))
|
||||
return false;
|
||||
// TODO(zhafang): Add "custom_id" to attribute metadata.
|
||||
// TODO(draco-eng): Add "custom_id" to attribute metadata.
|
||||
unique_id = static_cast<uint32_t>(custom_id);
|
||||
ga.set_unique_id(unique_id);
|
||||
} else
|
||||
|
@ -49,8 +49,9 @@ class PointAttributeVectorOutputIterator {
|
||||
uint32_t required_decode_bytes = 0;
|
||||
for (auto index = 0; index < attributes_.size(); index++) {
|
||||
const AttributeTuple &att = attributes_[index];
|
||||
required_decode_bytes = (std::max)(required_decode_bytes,
|
||||
std::get<1>(att) * std::get<3>(att));
|
||||
required_decode_bytes =
|
||||
(std::max)(required_decode_bytes,
|
||||
std::get<1>(att) * std::get<3>(att) * std::get<4>(att));
|
||||
}
|
||||
memory_.resize(required_decode_bytes);
|
||||
data_ = memory_.data();
|
||||
@ -102,8 +103,10 @@ class PointAttributeVectorOutputIterator {
|
||||
// redirect to copied data
|
||||
data_source = reinterpret_cast<uint32_t *>(data_);
|
||||
}
|
||||
attribute->SetAttributeValue(attribute->mapped_index(point_id_),
|
||||
data_source);
|
||||
const AttributeValueIndex avi = attribute->mapped_index(point_id_);
|
||||
if (avi >= attribute->size())
|
||||
return *this;
|
||||
attribute->SetAttributeValue(avi, data_source);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
@ -77,12 +77,14 @@ class MeshTraversalSequencer : public PointsSequencer {
|
||||
traverser_.OnTraversalStart();
|
||||
if (corner_order_) {
|
||||
for (uint32_t i = 0; i < corner_order_->size(); ++i) {
|
||||
ProcessCorner(corner_order_->at(i));
|
||||
if (!ProcessCorner(corner_order_->at(i)))
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
const int32_t num_faces = traverser_.corner_table()->num_faces();
|
||||
for (int i = 0; i < num_faces; ++i) {
|
||||
ProcessCorner(CornerIndex(3 * i));
|
||||
if (!ProcessCorner(CornerIndex(3 * i)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
traverser_.OnTraversalEnd();
|
||||
@ -90,8 +92,8 @@ class MeshTraversalSequencer : public PointsSequencer {
|
||||
}
|
||||
|
||||
private:
|
||||
void ProcessCorner(CornerIndex corner_id) {
|
||||
traverser_.TraverseFromCorner(corner_id);
|
||||
bool ProcessCorner(CornerIndex corner_id) {
|
||||
return traverser_.TraverseFromCorner(corner_id);
|
||||
}
|
||||
|
||||
TraverserT traverser_;
|
||||
|
@ -152,8 +152,10 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramDecoder<
|
||||
// Check which parallelograms are actually used.
|
||||
for (int i = 0; i < num_parallelograms; ++i) {
|
||||
const int context = num_parallelograms - 1;
|
||||
const bool is_crease =
|
||||
is_crease_edge_[context][is_crease_edge_pos[context]++];
|
||||
const int pos = is_crease_edge_pos[context]++;
|
||||
if (is_crease_edge_[context].size() <= pos)
|
||||
return false;
|
||||
const bool is_crease = is_crease_edge_[context][pos];
|
||||
if (!is_crease) {
|
||||
++num_used_parallelograms;
|
||||
for (int j = 0; j < num_components; ++j) {
|
||||
@ -207,7 +209,8 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramDecoder<
|
||||
if (num_flags > 0) {
|
||||
is_crease_edge_[i].resize(num_flags);
|
||||
RAnsBitDecoder decoder;
|
||||
decoder.StartDecoding(buffer);
|
||||
if (!decoder.StartDecoding(buffer))
|
||||
return false;
|
||||
for (int j = 0; j < num_flags; ++j) {
|
||||
is_crease_edge_[i][j] = decoder.DecodeNextBit();
|
||||
}
|
||||
|
@ -257,17 +257,19 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramEncoder<
|
||||
// Variable for holding the best configuration that has been found so far.
|
||||
PredictionConfiguration best_prediction;
|
||||
|
||||
total_parallelograms[num_parallelograms - 1] += num_parallelograms;
|
||||
// Compute delta coding error (configuration when no parallelogram is
|
||||
// selected).
|
||||
const int src_offset = (p - 1) * num_components;
|
||||
error = ComputeError(in_data + src_offset, in_data + dst_offset,
|
||||
¤t_residuals[0], num_components);
|
||||
|
||||
int64_t new_overhead_bits =
|
||||
ComputeOverheadBits(total_used_parallelograms[num_parallelograms - 1],
|
||||
total_parallelograms[num_parallelograms - 1]);
|
||||
error.num_bits += new_overhead_bits;
|
||||
if (num_parallelograms > 0) {
|
||||
total_parallelograms[num_parallelograms - 1] += num_parallelograms;
|
||||
const int64_t new_overhead_bits =
|
||||
ComputeOverheadBits(total_used_parallelograms[num_parallelograms - 1],
|
||||
total_parallelograms[num_parallelograms - 1]);
|
||||
error.num_bits += new_overhead_bits;
|
||||
}
|
||||
|
||||
best_prediction.error = error;
|
||||
best_prediction.configuration = 0;
|
||||
@ -312,13 +314,15 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramEncoder<
|
||||
}
|
||||
error = ComputeError(multi_pred_vals.data(), in_data + dst_offset,
|
||||
¤t_residuals[0], num_components);
|
||||
new_overhead_bits = ComputeOverheadBits(
|
||||
total_used_parallelograms[num_parallelograms - 1] +
|
||||
num_used_parallelograms,
|
||||
total_parallelograms[num_parallelograms - 1]);
|
||||
if (num_parallelograms > 0) {
|
||||
const int64_t new_overhead_bits = ComputeOverheadBits(
|
||||
total_used_parallelograms[num_parallelograms - 1] +
|
||||
num_used_parallelograms,
|
||||
total_parallelograms[num_parallelograms - 1]);
|
||||
|
||||
// Add overhead bits to the total error.
|
||||
error.num_bits += new_overhead_bits;
|
||||
// Add overhead bits to the total error.
|
||||
error.num_bits += new_overhead_bits;
|
||||
}
|
||||
if (error < best_prediction.error) {
|
||||
best_prediction.error = error;
|
||||
best_prediction.configuration = configuration;
|
||||
@ -331,8 +335,10 @@ bool MeshPredictionSchemeConstrainedMultiParallelogramEncoder<
|
||||
} while (std::next_permutation(
|
||||
exluded_parallelograms, exluded_parallelograms + num_parallelograms));
|
||||
}
|
||||
total_used_parallelograms[num_parallelograms - 1] +=
|
||||
best_prediction.num_used_parallelograms;
|
||||
if (num_parallelograms > 0) {
|
||||
total_used_parallelograms[num_parallelograms - 1] +=
|
||||
best_prediction.num_used_parallelograms;
|
||||
}
|
||||
|
||||
// Update the entropy stream by adding selected residuals as symbols to the
|
||||
// stream.
|
||||
|
@ -148,6 +148,8 @@ bool MeshPredictionSchemeTexCoordsDecoder<DataTypeT, TransformT, MeshDataT>::
|
||||
if (!DecodeVarint(&num_orientations, buffer))
|
||||
return false;
|
||||
}
|
||||
if (num_orientations == 0)
|
||||
return false;
|
||||
orientations_.resize(num_orientations);
|
||||
bool last_orientation = true;
|
||||
RAnsBitDecoder decoder;
|
||||
|
@ -90,7 +90,9 @@ bool MeshPredictionSchemeTexCoordsPortableDecoder<
|
||||
const int corner_map_size = 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);
|
||||
predictor_.template ComputePredictedValue<false>(corner_id, out_data, p);
|
||||
if (!predictor_.template ComputePredictedValue<false>(corner_id, out_data,
|
||||
p))
|
||||
return false;
|
||||
|
||||
const int dst_offset = p * num_components;
|
||||
this->transform().ComputeOriginalValue(predictor_.predicted_value(),
|
||||
|
@ -57,7 +57,7 @@ class MeshPredictionSchemeTexCoordsPortablePredictor {
|
||||
// Computes predicted UV coordinates on a given corner. The coordinates are
|
||||
// stored in |predicted_value_| member.
|
||||
template <bool is_encoder_t>
|
||||
void ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data,
|
||||
bool ComputePredictedValue(CornerIndex corner_id, const DataTypeT *data,
|
||||
int data_id);
|
||||
|
||||
const DataTypeT *predicted_value() const { return predicted_value_; }
|
||||
@ -82,7 +82,7 @@ class MeshPredictionSchemeTexCoordsPortablePredictor {
|
||||
|
||||
template <typename DataTypeT, class MeshDataT>
|
||||
template <bool is_encoder_t>
|
||||
void MeshPredictionSchemeTexCoordsPortablePredictor<
|
||||
bool MeshPredictionSchemeTexCoordsPortablePredictor<
|
||||
DataTypeT, MeshDataT>::ComputePredictedValue(CornerIndex corner_id,
|
||||
const DataTypeT *data,
|
||||
int data_id) {
|
||||
@ -111,7 +111,7 @@ void MeshPredictionSchemeTexCoordsPortablePredictor<
|
||||
// We cannot do a reliable prediction on degenerated UV triangles.
|
||||
predicted_value_[0] = p_uv[0];
|
||||
predicted_value_[1] = p_uv[1];
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get positions at all corners.
|
||||
@ -203,6 +203,8 @@ void MeshPredictionSchemeTexCoordsPortablePredictor<
|
||||
}
|
||||
} else {
|
||||
// When decoding the data, we already know which orientation to use.
|
||||
if (orientations_.empty())
|
||||
return false;
|
||||
const bool orientation = orientations_.back();
|
||||
orientations_.pop_back();
|
||||
if (orientation)
|
||||
@ -212,7 +214,7 @@ void MeshPredictionSchemeTexCoordsPortablePredictor<
|
||||
}
|
||||
predicted_value_[0] = static_cast<int>(predicted_uv[0]);
|
||||
predicted_value_[1] = static_cast<int>(predicted_uv[1]);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Else we don't have available textures on both corners or the position data
|
||||
@ -236,12 +238,13 @@ void MeshPredictionSchemeTexCoordsPortablePredictor<
|
||||
for (int i = 0; i < kNumComponents; ++i) {
|
||||
predicted_value_[i] = 0;
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < kNumComponents; ++i) {
|
||||
predicted_value_[i] = data[data_offset + i];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
@ -48,8 +48,9 @@ class PredictionSchemeNormalOctahedronCanonicalizedDecodingTransform
|
||||
return false;
|
||||
if (!buffer->Decode(¢er_value))
|
||||
return false;
|
||||
this->set_max_quantized_value(max_quantized_value);
|
||||
(void)center_value;
|
||||
if (!this->set_max_quantized_value(max_quantized_value))
|
||||
return false;
|
||||
// Account for reading wrong values, e.g., due to fuzzing.
|
||||
if (this->quantization_bits() < 2)
|
||||
return false;
|
||||
|
@ -51,9 +51,8 @@ class PredictionSchemeNormalOctahedronDecodingTransform
|
||||
if (!buffer->Decode(¢er_value))
|
||||
return false;
|
||||
}
|
||||
this->set_max_quantized_value(max_quantized_value);
|
||||
(void)center_value;
|
||||
return true;
|
||||
return this->set_max_quantized_value(max_quantized_value);
|
||||
}
|
||||
|
||||
inline void ComputeOriginalValue(const DataType *pred_vals,
|
||||
|
@ -59,10 +59,11 @@ class PredictionSchemeNormalOctahedronTransformBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
inline void set_max_quantized_value(DataTypeT max_quantized_value) {
|
||||
DRACO_DCHECK_EQ(max_quantized_value % 2, 1);
|
||||
inline bool set_max_quantized_value(DataTypeT max_quantized_value) {
|
||||
if (max_quantized_value % 2 == 0)
|
||||
return false;
|
||||
int q = bits::MostSignificantBit(max_quantized_value) + 1;
|
||||
octahedron_tool_box_.SetQuantizationBits(q);
|
||||
return octahedron_tool_box_.SetQuantizationBits(q);
|
||||
}
|
||||
|
||||
bool IsInDiamond(DataTypeT s, DataTypeT t) const {
|
||||
|
@ -36,7 +36,7 @@ bool SequentialAttributeDecoder::InitializeStandalone(
|
||||
|
||||
bool SequentialAttributeDecoder::DecodePortableAttribute(
|
||||
const std::vector<PointIndex> &point_ids, DecoderBuffer *in_buffer) {
|
||||
if (!attribute_->Reset(point_ids.size()))
|
||||
if (attribute_->num_components() <= 0 || !attribute_->Reset(point_ids.size()))
|
||||
return false;
|
||||
if (!DecodeValues(point_ids, in_buffer))
|
||||
return false;
|
||||
@ -83,7 +83,8 @@ bool SequentialAttributeDecoder::InitPredictionScheme(
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (!ps->SetParentAttribute(decoder_->GetPortableAttribute(att_id))) {
|
||||
const PointAttribute *const pa = decoder_->GetPortableAttribute(att_id);
|
||||
if (pa == nullptr || !ps->SetParentAttribute(pa)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -92,6 +92,8 @@ bool SequentialIntegerAttributeDecoder::DecodeIntegerValues(
|
||||
const size_t num_values = num_entries * num_components;
|
||||
PreparePortableAttribute(num_entries, num_components);
|
||||
int32_t *const portable_attribute_data = GetPortableAttributeData();
|
||||
if (portable_attribute_data == nullptr)
|
||||
return false;
|
||||
uint8_t compressed;
|
||||
if (!in_buffer->Decode(&compressed))
|
||||
return false;
|
||||
|
@ -56,6 +56,8 @@ class SequentialIntegerAttributeDecoder : public SequentialAttributeDecoder {
|
||||
void PreparePortableAttribute(int num_entries, int num_components);
|
||||
|
||||
int32_t *GetPortableAttributeData() {
|
||||
if (portable_attribute()->size() == 0)
|
||||
return nullptr;
|
||||
return reinterpret_cast<int32_t *>(
|
||||
portable_attribute()->GetAddress(AttributeValueIndex(0)));
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ static constexpr uint16_t kDracoMeshBitstreamVersion = DRACO_BITSTREAM_VERSION(
|
||||
kDracoMeshBitstreamVersionMajor, kDracoMeshBitstreamVersionMinor);
|
||||
|
||||
// Currently, we support point cloud and triangular mesh encoding.
|
||||
// TODO(draco-eng) convert enum to enum class (safety, not performance).
|
||||
// TODO(draco-eng) Convert enum to enum class (safety, not performance).
|
||||
enum EncodedGeometryType {
|
||||
INVALID_GEOMETRY_TYPE = -1,
|
||||
POINT_CLOUD = 0,
|
||||
@ -115,8 +115,7 @@ enum PredictionSchemeTransformType {
|
||||
enum MeshTraversalMethod {
|
||||
MESH_TRAVERSAL_DEPTH_FIRST = 0,
|
||||
MESH_TRAVERSAL_PREDICTION_DEGREE = 1,
|
||||
MESH_TRAVERSAL_RESERVED_1 = 2,
|
||||
MESH_TRAVERSAL_RESERVED_2 = 3,
|
||||
NUM_TRAVERSAL_METHODS
|
||||
};
|
||||
|
||||
// List of all variant of the edgebreaker method that is used for compression
|
||||
|
@ -30,10 +30,13 @@ Status Encoder::EncodePointCloudToBuffer(const PointCloud &pc,
|
||||
Status Encoder::EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer) {
|
||||
ExpertEncoder encoder(m);
|
||||
encoder.Reset(CreateExpertEncoderOptions(m));
|
||||
return encoder.EncodeToBuffer(out_buffer);
|
||||
DRACO_RETURN_IF_ERROR(encoder.EncodeToBuffer(out_buffer));
|
||||
set_num_encoded_points(encoder.num_encoded_points());
|
||||
set_num_encoded_faces(encoder.num_encoded_faces());
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
EncoderOptions Encoder::CreateExpertEncoderOptions(const PointCloud &pc) {
|
||||
EncoderOptions Encoder::CreateExpertEncoderOptions(const PointCloud &pc) const {
|
||||
EncoderOptions ret_options = EncoderOptions::CreateEmptyOptions();
|
||||
ret_options.SetGlobalOptions(options().GetGlobalOptions());
|
||||
ret_options.SetFeatureOptions(options().GetFeaturelOptions());
|
||||
|
@ -38,13 +38,14 @@ class Encoder
|
||||
typedef EncoderBase<EncoderOptionsBase<GeometryAttribute::Type>> Base;
|
||||
|
||||
Encoder();
|
||||
virtual ~Encoder() {}
|
||||
|
||||
// Encodes a point cloud to the provided buffer.
|
||||
Status EncodePointCloudToBuffer(const PointCloud &pc,
|
||||
EncoderBuffer *out_buffer);
|
||||
virtual Status EncodePointCloudToBuffer(const PointCloud &pc,
|
||||
EncoderBuffer *out_buffer);
|
||||
|
||||
// Encodes a mesh to the provided buffer.
|
||||
Status EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer);
|
||||
virtual Status EncodeMeshToBuffer(const Mesh &m, EncoderBuffer *out_buffer);
|
||||
|
||||
// Set encoder options used during the geometry encoding. Note that this call
|
||||
// overwrites any modifications to the options done with the functions below,
|
||||
@ -128,10 +129,10 @@ class Encoder
|
||||
// call of EncodePointCloudToBuffer or EncodeMeshToBuffer is going to fail.
|
||||
void SetEncodingMethod(int encoding_method);
|
||||
|
||||
private:
|
||||
protected:
|
||||
// Creates encoder options for the expert encoder used during the actual
|
||||
// encoding.
|
||||
EncoderOptions CreateExpertEncoderOptions(const PointCloud &pc);
|
||||
EncoderOptions CreateExpertEncoderOptions(const PointCloud &pc) const;
|
||||
};
|
||||
|
||||
} // namespace draco
|
||||
|
@ -29,11 +29,25 @@ class EncoderBase {
|
||||
public:
|
||||
typedef EncoderOptionsT OptionsType;
|
||||
|
||||
EncoderBase() : options_(EncoderOptionsT::CreateDefaultOptions()) {}
|
||||
EncoderBase()
|
||||
: options_(EncoderOptionsT::CreateDefaultOptions()),
|
||||
num_encoded_points_(0),
|
||||
num_encoded_faces_(0) {}
|
||||
virtual ~EncoderBase() {}
|
||||
|
||||
const EncoderOptionsT &options() const { return options_; }
|
||||
EncoderOptionsT &options() { return options_; }
|
||||
|
||||
// If enabled, it tells the encoder to keep track of the number of encoded
|
||||
// points and faces (default = false).
|
||||
// Note that this can slow down encoding for certain encoders.
|
||||
void SetTrackEncodedProperties(bool flag);
|
||||
|
||||
// Returns the number of encoded points and faces during the last encoding
|
||||
// operation. Returns 0 if SetTrackEncodedProperties() was not set.
|
||||
size_t num_encoded_points() const { return num_encoded_points_; }
|
||||
size_t num_encoded_faces() const { return num_encoded_faces_; }
|
||||
|
||||
protected:
|
||||
void Reset(const EncoderOptionsT &options) { options_ = options; }
|
||||
|
||||
@ -48,14 +62,17 @@ class EncoderBase {
|
||||
}
|
||||
|
||||
Status CheckPredictionScheme(GeometryAttribute::Type att_type,
|
||||
int prediction_scheme) {
|
||||
int prediction_scheme) const {
|
||||
// Out of bound checks:
|
||||
if (prediction_scheme < 0)
|
||||
return Status(Status::ERROR, "Invalid prediction scheme requested.");
|
||||
if (prediction_scheme >= NUM_PREDICTION_SCHEMES)
|
||||
return Status(Status::ERROR, "Invalid prediction scheme requested.");
|
||||
// Deprecated prediction schemes:
|
||||
if (prediction_scheme == MESH_PREDICTION_TEX_COORDS_DEPRECATED)
|
||||
return Status(Status::ERROR,
|
||||
"MESH_PREDICTION_TEX_COORDS_DEPRECATED is deprecated.");
|
||||
// Attribute specific checks:
|
||||
if (prediction_scheme == MESH_PREDICTION_TEX_COORDS_PORTABLE) {
|
||||
if (att_type != GeometryAttribute::TEX_COORD)
|
||||
return Status(Status::ERROR,
|
||||
@ -66,13 +83,26 @@ class EncoderBase {
|
||||
return Status(Status::ERROR,
|
||||
"Invalid prediction scheme for attribute type.");
|
||||
}
|
||||
return Status();
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
protected:
|
||||
void set_num_encoded_points(size_t num) { num_encoded_points_ = num; }
|
||||
void set_num_encoded_faces(size_t num) { num_encoded_faces_ = num; }
|
||||
|
||||
private:
|
||||
EncoderOptionsT options_;
|
||||
|
||||
size_t num_encoded_points_;
|
||||
size_t num_encoded_faces_;
|
||||
};
|
||||
|
||||
template <class EncoderOptionsT>
|
||||
void EncoderBase<EncoderOptionsT>::SetTrackEncodedProperties(bool flag) {
|
||||
options_.SetGlobalBool("store_number_of_encoded_points", flag);
|
||||
options_.SetGlobalBool("store_number_of_encoded_faces", flag);
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
||||
#endif // DRACO_SRC_DRACO_COMPRESSION_ENCODE_BASE_H_
|
||||
|
@ -135,6 +135,60 @@ class EncodeTest : public ::testing::Test {
|
||||
ASSERT_EQ(GetQuantizationBitsFromAttribute(mesh->attribute(2)),
|
||||
tex_coord_1_quantization);
|
||||
}
|
||||
|
||||
// Tests that the encoder returns the correct number of encoded points and
|
||||
// faces for a given mesh or point cloud.
|
||||
void TestNumberOfEncodedEntries(const std::string &file_name,
|
||||
int32_t encoding_method) {
|
||||
std::unique_ptr<draco::PointCloud> geometry;
|
||||
draco::Mesh *mesh = nullptr;
|
||||
|
||||
if (encoding_method == draco::MESH_EDGEBREAKER_ENCODING ||
|
||||
encoding_method == draco::MESH_SEQUENTIAL_ENCODING) {
|
||||
std::unique_ptr<draco::Mesh> mesh_tmp =
|
||||
draco::ReadMeshFromTestFile(file_name);
|
||||
mesh = mesh_tmp.get();
|
||||
geometry = std::move(mesh_tmp);
|
||||
} else {
|
||||
geometry = draco::ReadPointCloudFromTestFile(file_name);
|
||||
}
|
||||
ASSERT_NE(mesh, nullptr);
|
||||
draco::Encoder encoder;
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 16);
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::TEX_COORD, 15);
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::NORMAL, 14);
|
||||
encoder.SetEncodingMethod(encoding_method);
|
||||
|
||||
encoder.SetTrackEncodedProperties(true);
|
||||
|
||||
draco::EncoderBuffer buffer;
|
||||
if (mesh) {
|
||||
encoder.EncodeMeshToBuffer(*mesh, &buffer);
|
||||
} else {
|
||||
encoder.EncodePointCloudToBuffer(*geometry, &buffer);
|
||||
}
|
||||
|
||||
// Ensure the logged number of encoded points and faces matches the number
|
||||
// we get from the decoder.
|
||||
|
||||
draco::DecoderBuffer decoder_buffer;
|
||||
decoder_buffer.Init(buffer.data(), buffer.size());
|
||||
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();
|
||||
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();
|
||||
ASSERT_EQ(decoded_pc->num_points(), encoder.num_encoded_points());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(EncodeTest, TestExpertEncoderQuantization) {
|
||||
@ -204,4 +258,31 @@ TEST_F(EncodeTest, TestKdTreeEncoding) {
|
||||
ASSERT_TRUE(encoder.EncodePointCloudToBuffer(*pc, &buffer).ok());
|
||||
}
|
||||
|
||||
TEST_F(EncodeTest, TestTrackingOfNumberOfEncodedEntries) {
|
||||
TestNumberOfEncodedEntries("deg_faces.obj", draco::MESH_EDGEBREAKER_ENCODING);
|
||||
TestNumberOfEncodedEntries("deg_faces.obj", draco::MESH_SEQUENTIAL_ENCODING);
|
||||
TestNumberOfEncodedEntries("cube_att.obj", draco::MESH_EDGEBREAKER_ENCODING);
|
||||
TestNumberOfEncodedEntries("test_nm.obj", draco::MESH_EDGEBREAKER_ENCODING);
|
||||
TestNumberOfEncodedEntries("test_nm.obj", draco::MESH_SEQUENTIAL_ENCODING);
|
||||
TestNumberOfEncodedEntries("cube_subd.obj",
|
||||
draco::POINT_CLOUD_KD_TREE_ENCODING);
|
||||
TestNumberOfEncodedEntries("cube_subd.obj",
|
||||
draco::POINT_CLOUD_SEQUENTIAL_ENCODING);
|
||||
}
|
||||
|
||||
TEST_F(EncodeTest, TestTrackingOfNumberOfEncodedEntriesNotSet) {
|
||||
// Tests that when tracing of encoded properties is disabled, the returned
|
||||
// number of encoded faces and poitns is 0.
|
||||
std::unique_ptr<draco::Mesh> mesh(
|
||||
draco::ReadMeshFromTestFile("cube_att.obj"));
|
||||
ASSERT_NE(mesh, nullptr);
|
||||
|
||||
draco::EncoderBuffer buffer;
|
||||
draco::Encoder encoder;
|
||||
|
||||
ASSERT_TRUE(encoder.EncodeMeshToBuffer(*mesh, &buffer).ok());
|
||||
ASSERT_EQ(encoder.num_encoded_points(), 0);
|
||||
ASSERT_EQ(encoder.num_encoded_faces(), 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
@ -82,7 +82,11 @@ Status ExpertEncoder::EncodePointCloudToBuffer(const PointCloud &pc,
|
||||
encoder.reset(new PointCloudSequentialEncoder());
|
||||
}
|
||||
encoder->SetPointCloud(pc);
|
||||
return encoder->Encode(options(), out_buffer);
|
||||
DRACO_RETURN_IF_ERROR(encoder->Encode(options(), out_buffer));
|
||||
|
||||
set_num_encoded_points(encoder->num_encoded_points());
|
||||
set_num_encoded_faces(0);
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
Status ExpertEncoder::EncodeMeshToBuffer(const Mesh &m,
|
||||
@ -104,7 +108,11 @@ Status ExpertEncoder::EncodeMeshToBuffer(const Mesh &m,
|
||||
encoder = std::unique_ptr<MeshEncoder>(new MeshSequentialEncoder());
|
||||
}
|
||||
encoder->SetMesh(m);
|
||||
return encoder->Encode(options(), out_buffer);
|
||||
DRACO_RETURN_IF_ERROR(encoder->Encode(options(), out_buffer));
|
||||
|
||||
set_num_encoded_points(encoder->num_encoded_points());
|
||||
set_num_encoded_faces(encoder->num_encoded_faces());
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
void ExpertEncoder::Reset(const EncoderOptions &options) {
|
||||
@ -145,9 +153,10 @@ void ExpertEncoder::SetEncodingMethod(int encoding_method) {
|
||||
|
||||
Status ExpertEncoder::SetAttributePredictionScheme(
|
||||
int32_t attribute_id, int prediction_scheme_method) {
|
||||
auto att = point_cloud_->GetAttributeByUniqueId(attribute_id);
|
||||
auto att = point_cloud_->attribute(attribute_id);
|
||||
auto att_type = att->attribute_type();
|
||||
Status status = CheckPredictionScheme(att_type, prediction_scheme_method);
|
||||
const Status status =
|
||||
CheckPredictionScheme(att_type, prediction_scheme_method);
|
||||
if (!status.ok())
|
||||
return status;
|
||||
options().SetAttributeInt(attribute_id, "prediction_scheme",
|
||||
|
@ -48,7 +48,8 @@ MeshEdgeBreakerDecoderImpl<TraversalDecoder>::MeshEdgeBreakerDecoderImpl()
|
||||
last_vert_id_(-1),
|
||||
last_face_id_(-1),
|
||||
num_new_vertices_(0),
|
||||
num_encoded_vertices_(0) {}
|
||||
num_encoded_vertices_(0),
|
||||
pos_data_decoder_id_(-1) {}
|
||||
|
||||
template <class TraversalDecoder>
|
||||
bool MeshEdgeBreakerDecoderImpl<TraversalDecoder>::Init(
|
||||
@ -134,7 +135,18 @@ bool MeshEdgeBreakerDecoderImpl<TraversalDecoder>::CreateAttributesDecoder(
|
||||
if (att_data_id >= attribute_data_.size()) {
|
||||
return false; // Unexpected attribute data.
|
||||
}
|
||||
|
||||
// Ensure that the attribute data is not mapped to a different attributes
|
||||
// decoder already.
|
||||
if (attribute_data_[att_data_id].decoder_id >= 0)
|
||||
return false;
|
||||
|
||||
attribute_data_[att_data_id].decoder_id = att_decoder_id;
|
||||
} else {
|
||||
// Assign the attributes decoder to |pos_encoding_data_|.
|
||||
if (pos_data_decoder_id_ >= 0)
|
||||
return false; // Some other decoder is already using the data. Error.
|
||||
pos_data_decoder_id_ = att_decoder_id;
|
||||
}
|
||||
|
||||
MeshTraversalMethod traversal_method = MESH_TRAVERSAL_DEPTH_FIRST;
|
||||
@ -260,33 +272,13 @@ bool MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity() {
|
||||
if (num_faces > std::numeric_limits<CornerIndex::ValueType>::max() / 3)
|
||||
return false; // Draco cannot handle this many faces.
|
||||
|
||||
// Decode topology (connectivity).
|
||||
vertex_traversal_length_.clear();
|
||||
corner_table_ = std::unique_ptr<CornerTable>(new CornerTable());
|
||||
if (corner_table_ == nullptr)
|
||||
return false;
|
||||
processed_corner_ids_.clear();
|
||||
processed_corner_ids_.reserve(num_faces);
|
||||
processed_connectivity_corners_.clear();
|
||||
processed_connectivity_corners_.reserve(num_faces);
|
||||
topology_split_data_.clear();
|
||||
hole_event_data_.clear();
|
||||
init_face_configurations_.clear();
|
||||
init_corners_.clear();
|
||||
|
||||
last_symbol_id_ = -1;
|
||||
|
||||
last_face_id_ = -1;
|
||||
last_vert_id_ = -1;
|
||||
|
||||
if (num_encoded_vertices_ > num_faces * 3) {
|
||||
return false; // There cannot be more vertices than 3 * num_faces.
|
||||
}
|
||||
uint8_t num_attribute_data;
|
||||
if (!decoder_->buffer()->Decode(&num_attribute_data))
|
||||
return false;
|
||||
|
||||
attribute_data_.clear();
|
||||
// Add one attribute data for each attribute decoder.
|
||||
attribute_data_.resize(num_attribute_data);
|
||||
|
||||
uint32_t num_encoded_symbols;
|
||||
#ifdef DRACO_BACKWARDS_COMPATIBILITY_SUPPORTED
|
||||
if (decoder_->bitstream_version() < DRACO_BITSTREAM_VERSION(2, 0)) {
|
||||
@ -305,6 +297,14 @@ bool MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity() {
|
||||
// a symbol).
|
||||
return false;
|
||||
}
|
||||
const uint32_t max_encoded_faces =
|
||||
num_encoded_symbols + (num_encoded_symbols / 3);
|
||||
if (num_faces > max_encoded_faces) {
|
||||
// Faces can only be 1 1/3 times bigger than number of encoded symbols. This
|
||||
// could only happen if all new encoded components started with interior
|
||||
// triangles. E.g. A mesh with multiple tetrahedrons.
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t num_encoded_split_symbols;
|
||||
#ifdef DRACO_BACKWARDS_COMPATIBILITY_SUPPORTED
|
||||
@ -318,6 +318,32 @@ bool MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (num_encoded_split_symbols > num_encoded_symbols) {
|
||||
return false; // Split symbols are a sub-set of all symbols.
|
||||
}
|
||||
|
||||
// Decode topology (connectivity).
|
||||
vertex_traversal_length_.clear();
|
||||
corner_table_ = std::unique_ptr<CornerTable>(new CornerTable());
|
||||
if (corner_table_ == nullptr)
|
||||
return false;
|
||||
processed_corner_ids_.clear();
|
||||
processed_corner_ids_.reserve(num_faces);
|
||||
processed_connectivity_corners_.clear();
|
||||
processed_connectivity_corners_.reserve(num_faces);
|
||||
topology_split_data_.clear();
|
||||
hole_event_data_.clear();
|
||||
init_face_configurations_.clear();
|
||||
init_corners_.clear();
|
||||
|
||||
last_symbol_id_ = -1;
|
||||
last_face_id_ = -1;
|
||||
last_vert_id_ = -1;
|
||||
|
||||
attribute_data_.clear();
|
||||
// Add one attribute data for each attribute decoder.
|
||||
attribute_data_.resize(num_attribute_data);
|
||||
|
||||
if (!corner_table_->Reset(num_faces,
|
||||
num_encoded_vertices_ + num_encoded_split_symbols))
|
||||
return false;
|
||||
@ -515,8 +541,6 @@ int MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity(
|
||||
corner_table_->Vertex(corner_table_->Previous(corner_a));
|
||||
corner_table_->MapCornerToVertex(corner + 2, vert_a_prev);
|
||||
corner_table_->SetLeftMostCorner(vert_a_prev, corner + 2);
|
||||
if (corner_table_->num_vertices() > max_num_vertices)
|
||||
return -1; // Unexpected number of decoded vertices.
|
||||
// Mark the vertex |x| as interior.
|
||||
is_vert_hole_[vertex_x.value()] = false;
|
||||
// Update the corner on the active stack.
|
||||
@ -557,6 +581,10 @@ int MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity(
|
||||
SetOppositeCorners(opp_corner, corner_a);
|
||||
// Update vertex mapping.
|
||||
const VertexIndex new_vert_index = corner_table_->AddNewVertex();
|
||||
|
||||
if (corner_table_->num_vertices() > max_num_vertices)
|
||||
return -1; // Unexpected number of decoded vertices.
|
||||
|
||||
corner_table_->MapCornerToVertex(opp_corner, new_vert_index);
|
||||
corner_table_->SetLeftMostCorner(new_vert_index, opp_corner);
|
||||
|
||||
@ -596,6 +624,14 @@ int MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity(
|
||||
if (active_corner_stack.empty())
|
||||
return -1;
|
||||
const CornerIndex corner_a = active_corner_stack.back();
|
||||
|
||||
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.
|
||||
return -1;
|
||||
}
|
||||
|
||||
// First corner on the new face is corner "x" from the image above.
|
||||
const CornerIndex corner(3 * face.value());
|
||||
// Update the opposite corner mapping.
|
||||
@ -640,6 +676,10 @@ int MeshEdgeBreakerDecoderImpl<TraversalDecoder>::DecodeConnectivity(
|
||||
corner_table_->AddNewVertex());
|
||||
corner_table_->MapCornerToVertex(corner + 2,
|
||||
corner_table_->AddNewVertex());
|
||||
|
||||
if (corner_table_->num_vertices() > max_num_vertices)
|
||||
return -1; // Unexpected number of decoded vertices.
|
||||
|
||||
corner_table_->SetLeftMostCorner(first_vert_index, corner);
|
||||
corner_table_->SetLeftMostCorner(first_vert_index + 1, corner + 1);
|
||||
corner_table_->SetLeftMostCorner(first_vert_index + 2, corner + 2);
|
||||
@ -1028,6 +1068,8 @@ bool MeshEdgeBreakerDecoderImpl<TraversalDecoder>::AssignPointsToCorners(
|
||||
CornerIndex act_c = corner_table_->SwingRight(c);
|
||||
bool seam_found = false;
|
||||
while (act_c != c) {
|
||||
if (act_c == kInvalidCornerIndex)
|
||||
return false;
|
||||
if (attribute_data_[i].connectivity_data.Vertex(act_c) != vert_id) {
|
||||
// Attribute seam found. Stop.
|
||||
deduplication_first_corner = act_c;
|
||||
|
@ -199,6 +199,9 @@ class MeshEdgeBreakerDecoderImpl : public MeshEdgeBreakerDecoderImplInterface {
|
||||
|
||||
MeshAttributeIndicesEncodingData pos_encoding_data_;
|
||||
|
||||
// Id of an attributes decoder that uses |pos_encoding_data_|.
|
||||
int pos_data_decoder_id_;
|
||||
|
||||
// Data for non-position attributes used by the decoder.
|
||||
struct AttributeData {
|
||||
AttributeData() : decoder_id(-1), is_connectivity_used(true) {}
|
||||
|
@ -85,4 +85,68 @@ bool MeshEdgeBreakerEncoder::EncodeConnectivity() {
|
||||
return impl_->EncodeConnectivity();
|
||||
}
|
||||
|
||||
void MeshEdgeBreakerEncoder::ComputeNumberOfEncodedPoints() {
|
||||
if (!impl_)
|
||||
return;
|
||||
const CornerTable *const corner_table = impl_->GetCornerTable();
|
||||
if (!corner_table)
|
||||
return;
|
||||
size_t num_points =
|
||||
corner_table->num_vertices() - corner_table->NumIsolatedVertices();
|
||||
|
||||
if (mesh()->num_attributes() > 1) {
|
||||
// Add a new point based on the configuration of interior attribute seams
|
||||
// (replicating what the decoder would do).
|
||||
for (VertexIndex vi(0); vi < corner_table->num_vertices(); ++vi) {
|
||||
if (corner_table->IsVertexIsolated(vi))
|
||||
continue;
|
||||
// Go around all corners of the vertex and keep track of the observed
|
||||
// attribute seams.
|
||||
const CornerIndex first_corner_index = corner_table->LeftMostCorner(vi);
|
||||
const PointIndex first_point_index =
|
||||
mesh()->CornerToPointId(first_corner_index);
|
||||
|
||||
PointIndex last_point_index = first_point_index;
|
||||
CornerIndex corner_index = corner_table->SwingRight(first_corner_index);
|
||||
size_t num_attribute_seams = 0;
|
||||
while (corner_index != kInvalidCornerIndex &&
|
||||
corner_index != first_corner_index) {
|
||||
const PointIndex point_index = mesh()->CornerToPointId(corner_index);
|
||||
if (point_index != last_point_index) {
|
||||
// New attribute seam detected.
|
||||
++num_attribute_seams;
|
||||
last_point_index = point_index;
|
||||
}
|
||||
// Proceed to the next corner
|
||||
corner_index = corner_table->SwingRight(corner_index);
|
||||
}
|
||||
|
||||
if (!corner_table->IsOnBoundary(vi) && num_attribute_seams > 0 &&
|
||||
last_point_index == first_point_index) {
|
||||
// If the last visited point index is the same as the first point index
|
||||
// we traveled all the way around the vertex. In this case the number of
|
||||
// new points should be num_attribute_seams - 1
|
||||
num_points += num_attribute_seams - 1;
|
||||
} else {
|
||||
// Else the vertex was either on a boundary (i.e. we couldn't travel all
|
||||
// around the vertex), or we ended up at a different point. In both of
|
||||
// these cases, the number of new points is equal to the number of
|
||||
// attribute seams.
|
||||
num_points += num_attribute_seams;
|
||||
}
|
||||
}
|
||||
}
|
||||
set_num_encoded_points(num_points);
|
||||
}
|
||||
|
||||
void MeshEdgeBreakerEncoder::ComputeNumberOfEncodedFaces() {
|
||||
if (!impl_)
|
||||
return;
|
||||
const CornerTable *const corner_table = impl_->GetCornerTable();
|
||||
if (!corner_table)
|
||||
return;
|
||||
set_num_encoded_faces(corner_table->num_faces() -
|
||||
corner_table->NumDegeneratedFaces());
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
@ -54,6 +54,8 @@ class MeshEdgeBreakerEncoder : public MeshEncoder {
|
||||
bool EncodeConnectivity() override;
|
||||
bool GenerateAttributesEncoder(int32_t att_id) override;
|
||||
bool EncodeAttributesEncoderIdentifier(int32_t att_encoder_id) override;
|
||||
void ComputeNumberOfEncodedPoints() override;
|
||||
void ComputeNumberOfEncodedFaces() override;
|
||||
|
||||
private:
|
||||
// The actual implementation of the edge breaker method. The implementations
|
||||
|
@ -151,6 +151,13 @@ bool MeshEdgeBreakerEncoderImpl<TraversalEncoder>::GenerateAttributesEncoder(
|
||||
encoding_data = &pos_encoding_data_;
|
||||
} else {
|
||||
encoding_data = &attribute_data_[att_data_id].encoding_data;
|
||||
|
||||
// Ensure we use the correct number of vertices in the encoding data.
|
||||
encoding_data->vertex_to_encoded_attribute_value_index_map.assign(
|
||||
corner_table_->num_vertices(), -1);
|
||||
|
||||
// Mark the attribute specific connectivity data as not used as we use the
|
||||
// position attribute connectivity data.
|
||||
attribute_data_[att_data_id].is_connectivity_used = false;
|
||||
}
|
||||
|
||||
@ -165,14 +172,11 @@ bool MeshEdgeBreakerEncoderImpl<TraversalEncoder>::GenerateAttributesEncoder(
|
||||
traversal_method = MESH_TRAVERSAL_DEPTH_FIRST;
|
||||
}
|
||||
}
|
||||
// Select traverser that is used to generate the encoding order.
|
||||
if (traversal_method == MESH_TRAVERSAL_PREDICTION_DEGREE) {
|
||||
// Traverser that is used to generate the encoding order of each
|
||||
// attribute.
|
||||
typedef PredictionDegreeTraverser<AttProcessor, AttObserver> AttTraverser;
|
||||
sequencer = CreateVertexTraversalSequencer<AttTraverser>(encoding_data);
|
||||
} else {
|
||||
// Traverser that is used to generate the encoding order of each
|
||||
// attribute.
|
||||
} else if (traversal_method == MESH_TRAVERSAL_DEPTH_FIRST) {
|
||||
typedef EdgeBreakerTraverser<AttProcessor, AttObserver> AttTraverser;
|
||||
sequencer = CreateVertexTraversalSequencer<AttTraverser>(encoding_data);
|
||||
}
|
||||
@ -185,6 +189,11 @@ bool MeshEdgeBreakerEncoderImpl<TraversalEncoder>::GenerateAttributesEncoder(
|
||||
// Traverser that is used to generate the encoding order of each attribute.
|
||||
typedef EdgeBreakerTraverser<AttProcessor, AttObserver> AttTraverser;
|
||||
|
||||
// Ensure we use the correct number of vertices in the encoding data.
|
||||
attribute_data_[att_data_id]
|
||||
.encoding_data.vertex_to_encoded_attribute_value_index_map.assign(
|
||||
attribute_data_[att_data_id].connectivity_data.num_vertices(), -1);
|
||||
|
||||
std::unique_ptr<MeshTraversalSequencer<AttTraverser>> traversal_sequencer(
|
||||
new MeshTraversalSequencer<AttTraverser>(
|
||||
mesh_, &attribute_data_[att_data_id].encoding_data));
|
||||
@ -222,7 +231,7 @@ bool MeshEdgeBreakerEncoderImpl<TraversalEncoder>::GenerateAttributesEncoder(
|
||||
attribute_encoder_to_data_id_map_.push_back(att_data_id);
|
||||
GetEncoder()->AddAttributesEncoder(std::move(att_controller));
|
||||
return true;
|
||||
}
|
||||
} // namespace draco
|
||||
|
||||
template <class TraversalEncoder>
|
||||
bool MeshEdgeBreakerEncoderImpl<TraversalEncoder>::
|
||||
@ -768,9 +777,6 @@ bool MeshEdgeBreakerEncoderImpl<TraversalEncoder>::InitAttributeData() {
|
||||
attribute_data_[data_index]
|
||||
.encoding_data.encoded_attribute_value_index_to_corner_map.reserve(
|
||||
corner_table_->num_corners());
|
||||
attribute_data_[data_index]
|
||||
.encoding_data.vertex_to_encoded_attribute_value_index_map.assign(
|
||||
corner_table_->num_corners(), -1);
|
||||
attribute_data_[data_index].encoding_data.num_values = 0;
|
||||
attribute_data_[data_index].connectivity_data.InitFromAttribute(
|
||||
mesh_, corner_table_.get(), att);
|
||||
|
@ -156,12 +156,10 @@ class MeshEdgeBreakerTraversalDecoder {
|
||||
if (traversal_size > buffer_.remaining_size())
|
||||
return false;
|
||||
buffer_.Advance(traversal_size);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
start_face_decoder_.StartDecoding(&buffer_);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
return start_face_decoder_.StartDecoding(&buffer_);
|
||||
}
|
||||
|
||||
bool DecodeAttributeSeams() {
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
namespace draco {
|
||||
|
||||
MeshEncoder::MeshEncoder() : mesh_(nullptr) {}
|
||||
MeshEncoder::MeshEncoder() : mesh_(nullptr), num_encoded_faces_(0) {}
|
||||
|
||||
void MeshEncoder::SetMesh(const Mesh &m) {
|
||||
mesh_ = &m;
|
||||
@ -26,6 +26,8 @@ void MeshEncoder::SetMesh(const Mesh &m) {
|
||||
bool MeshEncoder::EncodeGeometryData() {
|
||||
if (!EncodeConnectivity())
|
||||
return false;
|
||||
if (options()->GetGlobalBool("store_number_of_encoded_faces", false))
|
||||
ComputeNumberOfEncodedFaces();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,11 @@ class MeshEncoder : public PointCloudEncoder {
|
||||
return TRIANGULAR_MESH;
|
||||
}
|
||||
|
||||
// Returns the number of faces that were encoded during the last Encode().
|
||||
// function call. Valid only if "store_number_of_encoded_faces" flag was set
|
||||
// in the provided EncoderOptions.
|
||||
size_t num_encoded_faces() const { return num_encoded_faces_; }
|
||||
|
||||
// Returns the base connectivity of the encoded mesh (or nullptr if it is not
|
||||
// initialized).
|
||||
virtual const CornerTable *GetCornerTable() const { return nullptr; }
|
||||
@ -61,10 +66,17 @@ class MeshEncoder : public PointCloudEncoder {
|
||||
// Needs to be implemented by the derived classes.
|
||||
virtual bool EncodeConnectivity() = 0;
|
||||
|
||||
// Computes and sets the num_encoded_faces_ for the encoder.
|
||||
virtual void ComputeNumberOfEncodedFaces() = 0;
|
||||
|
||||
void set_mesh(const Mesh *mesh) { mesh_ = mesh; }
|
||||
void set_num_encoded_faces(size_t num_faces) {
|
||||
num_encoded_faces_ = num_faces;
|
||||
}
|
||||
|
||||
private:
|
||||
const Mesh *mesh_;
|
||||
size_t num_encoded_faces_;
|
||||
};
|
||||
|
||||
} // namespace draco
|
||||
|
@ -120,4 +120,12 @@ bool MeshSequentialEncoder::CompressAndEncodeIndices() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void MeshSequentialEncoder::ComputeNumberOfEncodedPoints() {
|
||||
set_num_encoded_points(mesh()->num_points());
|
||||
}
|
||||
|
||||
void MeshSequentialEncoder::ComputeNumberOfEncodedFaces() {
|
||||
set_num_encoded_faces(mesh()->num_faces());
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
@ -44,6 +44,8 @@ class MeshSequentialEncoder : public MeshEncoder {
|
||||
protected:
|
||||
bool EncodeConnectivity() override;
|
||||
bool GenerateAttributesEncoder(int32_t att_id) override;
|
||||
void ComputeNumberOfEncodedPoints() override;
|
||||
void ComputeNumberOfEncodedFaces() override;
|
||||
|
||||
private:
|
||||
// Returns false on error.
|
||||
|
@ -82,6 +82,8 @@ class DynamicIntegerPointsKdTreeDecoder {
|
||||
public:
|
||||
explicit DynamicIntegerPointsKdTreeDecoder(uint32_t dimension)
|
||||
: bit_length_(0),
|
||||
num_points_(0),
|
||||
num_decoded_points_(0),
|
||||
dimension_(dimension),
|
||||
p_(dimension, 0),
|
||||
axes_(dimension, 0),
|
||||
@ -103,7 +105,7 @@ class DynamicIntegerPointsKdTreeDecoder {
|
||||
uint32_t last_axis);
|
||||
|
||||
template <class OutputIteratorT>
|
||||
void DecodeInternal(uint32_t num_points, OutputIteratorT &oit);
|
||||
bool DecodeInternal(uint32_t num_points, OutputIteratorT &oit);
|
||||
|
||||
void DecodeNumber(int nbits, uint32_t *value) {
|
||||
numbers_decoder_.DecodeLeastSignificantBits32(nbits, value);
|
||||
@ -123,6 +125,7 @@ class DynamicIntegerPointsKdTreeDecoder {
|
||||
|
||||
uint32_t bit_length_;
|
||||
uint32_t num_points_;
|
||||
uint32_t num_decoded_points_;
|
||||
uint32_t dimension_;
|
||||
NumbersDecoder numbers_decoder_;
|
||||
RemainingBitsDecoder remaining_bits_decoder_;
|
||||
@ -148,9 +151,12 @@ template <class OutputIteratorT>
|
||||
bool DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodePoints(
|
||||
DecoderBuffer *buffer, OutputIteratorT &oit) {
|
||||
buffer->Decode(&bit_length_);
|
||||
if (bit_length_ > 32)
|
||||
return false;
|
||||
buffer->Decode(&num_points_);
|
||||
if (num_points_ == 0)
|
||||
return true;
|
||||
num_decoded_points_ = 0;
|
||||
|
||||
if (!numbers_decoder_.StartDecoding(buffer))
|
||||
return false;
|
||||
@ -161,7 +167,8 @@ bool DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodePoints(
|
||||
if (!half_decoder_.StartDecoding(buffer))
|
||||
return false;
|
||||
|
||||
DecodeInternal(num_points_, oit);
|
||||
if (!DecodeInternal(num_points_, oit))
|
||||
return false;
|
||||
|
||||
numbers_decoder_.EndDecoding();
|
||||
remaining_bits_decoder_.EndDecoding();
|
||||
@ -194,7 +201,7 @@ uint32_t DynamicIntegerPointsKdTreeDecoder<compression_level_t>::GetAxis(
|
||||
|
||||
template <int compression_level_t>
|
||||
template <class OutputIteratorT>
|
||||
void DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodeInternal(
|
||||
bool DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodeInternal(
|
||||
uint32_t num_points, OutputIteratorT &oit) {
|
||||
typedef DecodingStatus Status;
|
||||
base_stack_[0] = VectorUint32(dimension_, 0);
|
||||
@ -214,14 +221,21 @@ void DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodeInternal(
|
||||
const VectorUint32 &old_base = base_stack_[stack_pos];
|
||||
const VectorUint32 &levels = levels_stack_[stack_pos];
|
||||
|
||||
if (num_remaining_points > num_points)
|
||||
return false;
|
||||
|
||||
const uint32_t axis = GetAxis(num_remaining_points, levels, last_axis);
|
||||
if (axis >= dimension_)
|
||||
return false;
|
||||
|
||||
const uint32_t level = levels[axis];
|
||||
|
||||
// All axes have been fully subdivided, just output points.
|
||||
if ((bit_length_ - level) == 0) {
|
||||
for (int i = 0; i < static_cast<int>(num_remaining_points); i++) {
|
||||
for (uint32_t i = 0; i < num_remaining_points; i++) {
|
||||
*oit = old_base;
|
||||
++oit;
|
||||
++num_decoded_points_;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@ -246,9 +260,14 @@ void DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodeInternal(
|
||||
}
|
||||
*oit = p_;
|
||||
++oit;
|
||||
++num_decoded_points_;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (num_decoded_points_ > num_points_)
|
||||
return false;
|
||||
|
||||
const int num_remaining_bits = bit_length_ - level;
|
||||
const uint32_t modifier = 1 << (num_remaining_bits - 1);
|
||||
base_stack_[stack_pos + 1] = old_base; // copy
|
||||
@ -273,6 +292,7 @@ void DynamicIntegerPointsKdTreeDecoder<compression_level_t>::DecodeInternal(
|
||||
if (second_half)
|
||||
status_stack.push(DecodingStatus(second_half, axis, stack_pos + 1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
extern template class DynamicIntegerPointsKdTreeDecoder<0>;
|
||||
|
@ -19,7 +19,7 @@
|
||||
namespace draco {
|
||||
|
||||
PointCloudEncoder::PointCloudEncoder()
|
||||
: point_cloud_(nullptr), buffer_(nullptr) {}
|
||||
: point_cloud_(nullptr), buffer_(nullptr), num_encoded_points_(0) {}
|
||||
|
||||
void PointCloudEncoder::SetPointCloud(const PointCloud &pc) {
|
||||
point_cloud_ = &pc;
|
||||
@ -47,6 +47,8 @@ Status PointCloudEncoder::Encode(const EncoderOptions &options,
|
||||
return Status(Status::ERROR, "Failed to encode geometry data.");
|
||||
if (!EncodePointAttributes())
|
||||
return Status(Status::ERROR, "Failed to encode point attributes.");
|
||||
if (options.GetGlobalBool("store_number_of_encoded_points", false))
|
||||
ComputeNumberOfEncodedPoints();
|
||||
return OkStatus();
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,11 @@ class PointCloudEncoder {
|
||||
// for mesh compression).
|
||||
virtual uint8_t GetEncodingMethod() const = 0;
|
||||
|
||||
// Returns the number of points that were encoded during the last Encode()
|
||||
// function call. Valid only if "store_number_of_encoded_points" flag was set
|
||||
// in the provided EncoderOptions.
|
||||
size_t num_encoded_points() const { return num_encoded_points_; }
|
||||
|
||||
int num_attributes_encoders() const { return attributes_encoders_.size(); }
|
||||
AttributesEncoder *attributes_encoder(int i) {
|
||||
return attributes_encoders_[i].get();
|
||||
@ -108,6 +113,13 @@ class PointCloudEncoder {
|
||||
// Encodes all the attribute data using the created attribute encoders.
|
||||
virtual bool EncodeAllAttributes();
|
||||
|
||||
// Computes and sets the num_encoded_points_ for the encoder.
|
||||
virtual void ComputeNumberOfEncodedPoints() = 0;
|
||||
|
||||
void set_num_encoded_points(size_t num_points) {
|
||||
num_encoded_points_ = num_points;
|
||||
}
|
||||
|
||||
private:
|
||||
// Encodes Draco header that is the same for all encoders.
|
||||
Status EncodeHeader();
|
||||
@ -135,6 +147,8 @@ class PointCloudEncoder {
|
||||
EncoderBuffer *buffer_;
|
||||
|
||||
const EncoderOptions *options_;
|
||||
|
||||
size_t num_encoded_points_;
|
||||
};
|
||||
|
||||
} // namespace draco
|
||||
|
@ -22,6 +22,8 @@ bool PointCloudKdTreeDecoder::DecodeGeometryData() {
|
||||
int32_t num_points;
|
||||
if (!buffer()->Decode(&num_points))
|
||||
return false;
|
||||
if (num_points < 0)
|
||||
return false;
|
||||
point_cloud()->set_num_points(num_points);
|
||||
return true;
|
||||
}
|
||||
|
@ -35,4 +35,8 @@ bool PointCloudKdTreeEncoder::GenerateAttributesEncoder(int32_t att_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PointCloudKdTreeEncoder::ComputeNumberOfEncodedPoints() {
|
||||
set_num_encoded_points(point_cloud()->num_points());
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
@ -37,6 +37,7 @@ class PointCloudKdTreeEncoder : public PointCloudEncoder {
|
||||
protected:
|
||||
bool EncodeGeometryData() override;
|
||||
bool GenerateAttributesEncoder(int32_t att_id) override;
|
||||
void ComputeNumberOfEncodedPoints() override;
|
||||
};
|
||||
|
||||
} // namespace draco
|
||||
|
@ -231,6 +231,69 @@ TEST_F(PointCloudKdTreeEncodingTest,
|
||||
TestKdTreeEncoding(*pc);
|
||||
}
|
||||
|
||||
// Test 16 and 8 bit encoding with size bigger than 32bit encoding.
|
||||
TEST_F(PointCloudKdTreeEncodingTest,
|
||||
TestIntKdTreeEncodingHigherDimensionVariedTypesBig16BitEncoding) {
|
||||
constexpr int num_points = 120;
|
||||
std::vector<std::array<uint32_t, 3>> points3(num_points);
|
||||
for (int i = 0; i < num_points; ++i) {
|
||||
std::array<uint32_t, 3> pos;
|
||||
// Generate some pseudo-random points.
|
||||
pos[0] = 8 * ((i * 7) % 127);
|
||||
pos[1] = 13 * ((i * 3) % 321);
|
||||
pos[2] = 29 * ((i * 19) % 450);
|
||||
points3[i] = pos;
|
||||
}
|
||||
// The total size of the 16bit encoding must be bigger than the total size of
|
||||
// the 32bit encoding.
|
||||
std::vector<std::array<uint16_t, 7>> points7(num_points);
|
||||
for (int i = 0; i < num_points; ++i) {
|
||||
std::array<uint16_t, 7> pos;
|
||||
// Generate some pseudo-random points.
|
||||
pos[0] = 8 * ((i * 7) % 127) + 1;
|
||||
pos[1] = 13 * ((i * 3) % 321) + 1;
|
||||
pos[2] = pos[0] + 13;
|
||||
pos[3] = pos[2] + 13;
|
||||
pos[4] = pos[3] + 13;
|
||||
pos[5] = pos[4] + 13;
|
||||
pos[6] = pos[5] + 13;
|
||||
points7[i] = pos;
|
||||
}
|
||||
std::vector<std::array<uint8_t, 1>> points1(num_points);
|
||||
for (int i = 0; i < num_points; ++i) {
|
||||
std::array<uint8_t, 1> pos;
|
||||
// Generate some pseudo-random points.
|
||||
pos[0] = 8 * ((i * 7) % 127) + 11;
|
||||
points1[i] = pos;
|
||||
}
|
||||
|
||||
PointCloudBuilder builder;
|
||||
builder.Start(num_points);
|
||||
const int att_id3 =
|
||||
builder.AddAttribute(GeometryAttribute::POSITION, 3, DT_UINT32);
|
||||
for (PointIndex i(0); i < num_points; ++i) {
|
||||
builder.SetAttributeValueForPoint(att_id3, PointIndex(i),
|
||||
&(points3[i.value()])[0]);
|
||||
}
|
||||
const int att_id2 =
|
||||
builder.AddAttribute(GeometryAttribute::POSITION, 7, DT_UINT16);
|
||||
for (PointIndex i(0); i < num_points; ++i) {
|
||||
builder.SetAttributeValueForPoint(att_id2, PointIndex(i),
|
||||
&(points7[i.value()])[0]);
|
||||
}
|
||||
const int att_id1 =
|
||||
builder.AddAttribute(GeometryAttribute::GENERIC, 1, DT_UINT8);
|
||||
for (PointIndex i(0); i < num_points; ++i) {
|
||||
builder.SetAttributeValueForPoint(att_id1, PointIndex(i),
|
||||
&(points1[i.value()])[0]);
|
||||
}
|
||||
|
||||
std::unique_ptr<PointCloud> pc = builder.Finalize(false);
|
||||
ASSERT_NE(pc, nullptr);
|
||||
|
||||
TestKdTreeEncoding(*pc);
|
||||
}
|
||||
|
||||
// Test encoding of quantized values.
|
||||
TEST_F(PointCloudKdTreeEncodingTest,
|
||||
TestIntKdTreeEncodingHigherDimensionFloatTypes) {
|
||||
|
@ -42,4 +42,8 @@ bool PointCloudSequentialEncoder::GenerateAttributesEncoder(int32_t att_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void PointCloudSequentialEncoder::ComputeNumberOfEncodedPoints() {
|
||||
set_num_encoded_points(point_cloud()->num_points());
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
@ -35,6 +35,7 @@ class PointCloudSequentialEncoder : public PointCloudEncoder {
|
||||
protected:
|
||||
bool EncodeGeometryData() override;
|
||||
bool GenerateAttributesEncoder(int32_t att_id) override;
|
||||
void ComputeNumberOfEncodedPoints() override;
|
||||
};
|
||||
|
||||
} // namespace draco
|
||||
|
@ -46,12 +46,14 @@ namespace draco {
|
||||
#endif
|
||||
|
||||
struct AnsCoder {
|
||||
AnsCoder() : buf(nullptr), buf_offset(0), state(0) {}
|
||||
uint8_t *buf;
|
||||
int buf_offset;
|
||||
uint32_t state;
|
||||
};
|
||||
|
||||
struct AnsDecoder {
|
||||
AnsDecoder() : buf(nullptr), buf_offset(0), state(0) {}
|
||||
const uint8_t *buf;
|
||||
int buf_offset;
|
||||
uint32_t state;
|
||||
|
@ -33,6 +33,9 @@ class DirectBitDecoder {
|
||||
// Decode one bit. Returns true if the bit is a 1, otherwise false.
|
||||
bool DecodeNextBit() {
|
||||
const uint32_t selector = 1 << (31 - num_used_bits_);
|
||||
if (pos_ == bits_.end()) {
|
||||
return false;
|
||||
}
|
||||
const bool bit = *pos_ & selector;
|
||||
++num_used_bits_;
|
||||
if (num_used_bits_ == 32) {
|
||||
@ -49,6 +52,10 @@ class DirectBitDecoder {
|
||||
DRACO_DCHECK_EQ(true, nbits > 0);
|
||||
const int remaining = 32 - num_used_bits_;
|
||||
if (nbits <= remaining) {
|
||||
if (pos_ == bits_.end()) {
|
||||
*value = 0;
|
||||
return;
|
||||
}
|
||||
*value = (*pos_ << num_used_bits_) >> (32 - nbits);
|
||||
num_used_bits_ += nbits;
|
||||
if (num_used_bits_ == 32) {
|
||||
@ -56,6 +63,10 @@ class DirectBitDecoder {
|
||||
num_used_bits_ = 0;
|
||||
}
|
||||
} else {
|
||||
if (pos_ + 1 == bits_.end()) {
|
||||
*value = 0;
|
||||
return;
|
||||
}
|
||||
const uint32_t value_l = ((*pos_) << num_used_bits_);
|
||||
num_used_bits_ = nbits - remaining;
|
||||
++pos_;
|
||||
|
@ -37,7 +37,7 @@ class DataBuffer {
|
||||
public:
|
||||
DataBuffer();
|
||||
bool Update(const void *data, int64_t size);
|
||||
// TODO(zhafang): The two update functions should be combined. I will
|
||||
// TODO(draco-eng): The two update functions should be combined. I will
|
||||
// leave for now in case it breaks any geometry compression tools.
|
||||
bool Update(const void *data, int64_t size, int64_t offset);
|
||||
|
||||
|
@ -23,12 +23,17 @@ namespace draco {
|
||||
|
||||
namespace {
|
||||
static constexpr char kTestDataDir[] = DRACO_TEST_DATA_DIR;
|
||||
static constexpr char kTestTempDir[] = DRACO_TEST_TEMP_DIR;
|
||||
} // namespace
|
||||
|
||||
std::string GetTestFileFullPath(const std::string &file_name) {
|
||||
return std::string(kTestDataDir) + std::string("/") + file_name;
|
||||
}
|
||||
|
||||
std::string GetTestTempFileFullPath(const std::string &file_name) {
|
||||
return std::string(kTestTempDir) + std::string("/") + file_name;
|
||||
}
|
||||
|
||||
bool GenerateGoldenFile(const std::string &golden_file_name, const void *data,
|
||||
int data_size) {
|
||||
const std::string path = GetTestFileFullPath(golden_file_name);
|
||||
|
@ -21,8 +21,13 @@
|
||||
|
||||
namespace draco {
|
||||
|
||||
// Returns the full path to a given test file.
|
||||
std::string GetTestFileFullPath(const std::string &file_name);
|
||||
// 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);
|
||||
|
||||
// Returns the full path to a given temporary file (a location where tests store
|
||||
// generated files).
|
||||
std::string GetTestTempFileFullPath(const std::string &file_name);
|
||||
|
||||
// Generates a new golden file and saves it into the correct folder.
|
||||
// Returns false if the file couldn't be created.
|
||||
|
@ -18,7 +18,7 @@
|
||||
namespace draco {
|
||||
|
||||
// Draco version is comprised of <major>.<minor>.<revision>.
|
||||
static const char kDracoVersion[] = "1.3.1";
|
||||
static const char kDracoVersion[] = "1.3.2";
|
||||
|
||||
const char *Version() { return kDracoVersion; }
|
||||
|
||||
|
@ -81,7 +81,8 @@ namespace draco {
|
||||
// #define FUNC_2(x, y) x + y
|
||||
// #define FUNC_3(x, y, z) x + y + z
|
||||
//
|
||||
// #define VARIADIC_MACRO(...) DRACO_SELECT_NTH_FROM_3(__VA_ARGS__, FUNC_3, FUNC_2, FUNC_1) __VA_ARGS__
|
||||
// #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
|
||||
|
@ -65,13 +65,14 @@ class Dequantizer {
|
||||
// Returns false when the initialization fails.
|
||||
bool Init(float range, int32_t max_quantized_value);
|
||||
inline float DequantizeFloat(int32_t val) const {
|
||||
const bool neg = (val < 0);
|
||||
if (neg) {
|
||||
val = -val;
|
||||
}
|
||||
float norm_value = static_cast<float>(val) * max_quantized_value_factor_;
|
||||
if (neg)
|
||||
norm_value = -norm_value;
|
||||
if (val >= 0)
|
||||
return static_cast<float>(val) * max_quantized_value_factor_ * range_;
|
||||
|
||||
// val was negative.
|
||||
const int64_t neg_val = -static_cast<int64_t>(val);
|
||||
float norm_value =
|
||||
static_cast<float>(neg_val) * max_quantized_value_factor_;
|
||||
norm_value = -norm_value;
|
||||
return norm_value * range_;
|
||||
}
|
||||
inline float operator()(int32_t val) const { return DequantizeFloat(val); }
|
||||
|
@ -37,6 +37,7 @@ bool ObjEncoder::EncodeToFile(const PointCloud &pc,
|
||||
std::ofstream file(file_name);
|
||||
if (!file)
|
||||
return false; // File could not be opened.
|
||||
file_name_ = file_name;
|
||||
// Encode the mesh into a buffer.
|
||||
EncoderBuffer buffer;
|
||||
if (!EncodeToBuffer(pc, &buffer))
|
||||
@ -99,6 +100,7 @@ bool ObjEncoder::ExitAndCleanup(bool return_value) {
|
||||
sub_obj_att_ = nullptr;
|
||||
current_sub_obj_id_ = -1;
|
||||
current_material_id_ = -1;
|
||||
file_name_.clear();
|
||||
return return_value;
|
||||
}
|
||||
|
||||
@ -127,13 +129,15 @@ bool ObjEncoder::GetSubObjects() {
|
||||
|
||||
bool ObjEncoder::EncodeMaterialFileName() {
|
||||
const GeometryMetadata *pc_metadata = in_point_cloud_->GetMetadata();
|
||||
if (!pc_metadata)
|
||||
return true;
|
||||
const AttributeMetadata *material_metadata =
|
||||
pc_metadata->GetAttributeMetadataByStringEntry("name", "material");
|
||||
const AttributeMetadata *material_metadata = nullptr;
|
||||
if (pc_metadata) {
|
||||
material_metadata =
|
||||
pc_metadata->GetAttributeMetadataByStringEntry("name", "material");
|
||||
}
|
||||
std::string material_file_name;
|
||||
std::string material_full_path;
|
||||
if (!material_metadata)
|
||||
return true;
|
||||
std::string material_file_name;
|
||||
if (!material_metadata->GetEntryString("file_name", &material_file_name))
|
||||
return false;
|
||||
buffer()->Encode("mtllib ", 7);
|
||||
|
@ -81,6 +81,8 @@ class ObjEncoder {
|
||||
std::unordered_map<int, std::string> material_id_to_name_;
|
||||
// Current material id of faces.
|
||||
int current_material_id_;
|
||||
|
||||
std::string file_name_;
|
||||
};
|
||||
|
||||
} // namespace draco
|
||||
|
@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include "draco/core/draco_test_base.h"
|
||||
@ -87,6 +88,8 @@ TEST_F(ObjEncoderTest, TestObjEncodingAll) {
|
||||
test_encoding("cube_quads.obj");
|
||||
test_encoding("cube_subd.obj");
|
||||
test_encoding("extra_vertex.obj");
|
||||
test_encoding("multiple_isolated_triangles.obj");
|
||||
test_encoding("multiple_tetrahedrons.obj");
|
||||
test_encoding("one_face_123.obj");
|
||||
test_encoding("one_face_312.obj");
|
||||
test_encoding("one_face_321.obj");
|
||||
@ -99,4 +102,5 @@ TEST_F(ObjEncoderTest, TestObjEncodingAll) {
|
||||
test_encoding("two_faces_123.obj");
|
||||
test_encoding("two_faces_312.obj");
|
||||
}
|
||||
|
||||
} // namespace draco
|
||||
|
@ -168,8 +168,15 @@ interface Encoder {
|
||||
[Const] float[] origin,
|
||||
float range);
|
||||
void SetSpeedOptions(long encoding_speed, long decoding_speed);
|
||||
void SetTrackEncodedProperties(boolean flag);
|
||||
|
||||
long EncodeMeshToDracoBuffer(Mesh mesh,
|
||||
DracoInt8Array encoded_data);
|
||||
long EncodePointCloudToDracoBuffer(PointCloud pc, boolean deduplicate_values,
|
||||
DracoInt8Array encoded_data);
|
||||
|
||||
// Returns the number of encoded points or faces from the last Encode
|
||||
// operation. Returns 0 if SetTrackEncodedProperties was not set to true.
|
||||
long GetNumberOfEncodedPoints();
|
||||
long GetNumberOfEncodedFaces();
|
||||
};
|
||||
|
@ -217,6 +217,10 @@ void Encoder::SetSpeedOptions(long encoding_speed, long decoding_speed) {
|
||||
encoder_.SetSpeedOptions(encoding_speed, decoding_speed);
|
||||
}
|
||||
|
||||
void Encoder::SetTrackEncodedProperties(bool flag) {
|
||||
encoder_.SetTrackEncodedProperties(flag);
|
||||
}
|
||||
|
||||
int Encoder::EncodeMeshToDracoBuffer(Mesh *mesh, DracoInt8Array *draco_buffer) {
|
||||
if (!mesh)
|
||||
return 0;
|
||||
@ -253,3 +257,9 @@ int Encoder::EncodePointCloudToDracoBuffer(draco::PointCloud *pc,
|
||||
draco_buffer->SetValues(buffer.data(), buffer.size());
|
||||
return buffer.size();
|
||||
}
|
||||
|
||||
int Encoder::GetNumberOfEncodedPoints() {
|
||||
return encoder_.num_encoded_points();
|
||||
}
|
||||
|
||||
int Encoder::GetNumberOfEncodedFaces() { return encoder_.num_encoded_faces(); }
|
||||
|
@ -109,8 +109,8 @@ class PointCloudBuilder {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO(zhafang): Regenerate wasm decoder.
|
||||
// TODO(zhafang): Add script to generate and test all Javascipt code.
|
||||
// TODO(draco-eng): Regenerate wasm decoder.
|
||||
// TODO(draco-eng): Add script to generate and test all Javascipt code.
|
||||
class MeshBuilder : public PointCloudBuilder {
|
||||
public:
|
||||
MeshBuilder();
|
||||
@ -145,12 +145,15 @@ class Encoder {
|
||||
long num_components,
|
||||
const float *origin, float range);
|
||||
void SetSpeedOptions(long encoding_speed, long decoding_speed);
|
||||
void SetTrackEncodedProperties(bool flag);
|
||||
|
||||
int EncodeMeshToDracoBuffer(draco::Mesh *mesh, DracoInt8Array *buffer);
|
||||
|
||||
int EncodePointCloudToDracoBuffer(draco::PointCloud *pc,
|
||||
bool deduplicate_values,
|
||||
DracoInt8Array *buffer);
|
||||
int GetNumberOfEncodedPoints();
|
||||
int GetNumberOfEncodedFaces();
|
||||
|
||||
private:
|
||||
draco::Encoder encoder_;
|
||||
|
@ -107,9 +107,9 @@ class EdgeBreakerTraverser {
|
||||
// Called when all the traversing is done.
|
||||
void OnTraversalEnd() {}
|
||||
|
||||
void TraverseFromCorner(CornerIndex corner_id) {
|
||||
bool TraverseFromCorner(CornerIndex corner_id) {
|
||||
if (processor_.IsFaceVisited(corner_id))
|
||||
return; // Already traversed.
|
||||
return true; // Already traversed.
|
||||
|
||||
corner_traversal_stack_.clear();
|
||||
corner_traversal_stack_.push_back(corner_id);
|
||||
@ -119,6 +119,8 @@ class EdgeBreakerTraverser {
|
||||
corner_table_->Vertex(corner_table_->Next(corner_id));
|
||||
const VertexIndex prev_vert =
|
||||
corner_table_->Vertex(corner_table_->Previous(corner_id));
|
||||
if (next_vert == kInvalidVertexIndex || prev_vert == kInvalidVertexIndex)
|
||||
return false;
|
||||
if (!processor_.IsVertexVisited(next_vert)) {
|
||||
processor_.MarkVertexVisited(next_vert);
|
||||
traversal_observer_.OnNewVertexVisited(next_vert,
|
||||
@ -146,6 +148,8 @@ class EdgeBreakerTraverser {
|
||||
processor_.MarkFaceVisited(face_id);
|
||||
traversal_observer_.OnNewFaceVisited(face_id);
|
||||
const VertexIndex vert_id = corner_table_->Vertex(corner_id);
|
||||
if (vert_id == kInvalidVertexIndex)
|
||||
return false;
|
||||
if (!processor_.IsVertexVisited(vert_id)) {
|
||||
const bool on_boundary = corner_table_->IsOnBoundary(vert_id);
|
||||
processor_.MarkVertexVisited(vert_id);
|
||||
@ -210,6 +214,7 @@ class EdgeBreakerTraverser {
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const CornerTable *corner_table() const { return corner_table_; }
|
||||
|
@ -69,9 +69,9 @@ class PredictionDegreeTraverser {
|
||||
// Called when all the traversing is done.
|
||||
void OnTraversalEnd() {}
|
||||
|
||||
void TraverseFromCorner(CornerIndex corner_id) {
|
||||
bool TraverseFromCorner(CornerIndex corner_id) {
|
||||
if (prediction_degree_.size() == 0)
|
||||
return;
|
||||
return true;
|
||||
|
||||
// Traversal starts from the |corner_id|. It's going to follow either the
|
||||
// right or the left neighboring faces to |corner_id| based on their
|
||||
@ -171,6 +171,7 @@ class PredictionDegreeTraverser {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const CornerTable *corner_table() const { return corner_table_; }
|
||||
|
@ -74,7 +74,7 @@ class GeometryMetadata : public Metadata {
|
||||
|
||||
const AttributeMetadata *GetAttributeMetadataByUniqueId(
|
||||
int32_t att_unique_id) const {
|
||||
// TODO(zhafang): Consider using unordered_map instead of vector to store
|
||||
// TODO(draco-eng): Consider using unordered_map instead of vector to store
|
||||
// attribute metadata.
|
||||
for (auto &&att_metadata : att_metadatas_) {
|
||||
if (att_metadata->att_unique_id() == att_unique_id) {
|
||||
@ -85,7 +85,7 @@ class GeometryMetadata : public Metadata {
|
||||
}
|
||||
|
||||
AttributeMetadata *attribute_metadata(int32_t att_unique_id) {
|
||||
// TODO(zhafang): Consider use unordered_map instead of vector to store
|
||||
// TODO(draco-eng): Consider use unordered_map instead of vector to store
|
||||
// attribute metadata.
|
||||
for (auto &&att_metadata : att_metadatas_) {
|
||||
if (att_metadata->att_unique_id() == att_unique_id) {
|
||||
|
@ -64,7 +64,7 @@ bool MetadataEncoder::EncodeGeometryMetadata(EncoderBuffer *out_buffer,
|
||||
// Encode number of attribute metadata.
|
||||
const std::vector<std::unique_ptr<AttributeMetadata>> &att_metadatas =
|
||||
metadata->attribute_metadatas();
|
||||
// TODO(zhafang): Limit the number of attributes.
|
||||
// TODO(draco-eng): Limit the number of attributes.
|
||||
EncodeVarint(static_cast<uint32_t>(att_metadatas.size()), out_buffer);
|
||||
// Encode each attribute metadata
|
||||
for (auto &&att_metadata : att_metadatas) {
|
||||
|
@ -35,6 +35,8 @@ struct Options {
|
||||
int generic_quantization_bits;
|
||||
bool generic_deleted;
|
||||
int compression_level;
|
||||
int edgebreaker_method;
|
||||
std::map<draco::GeometryAttribute::Type, int> attribute_prediction_scheme;
|
||||
bool use_metadata;
|
||||
std::string input;
|
||||
std::string output;
|
||||
@ -50,6 +52,7 @@ Options::Options()
|
||||
generic_quantization_bits(8),
|
||||
generic_deleted(false),
|
||||
compression_level(7),
|
||||
edgebreaker_method(-1),
|
||||
use_metadata(false) {}
|
||||
|
||||
void Usage() {
|
||||
@ -157,7 +160,7 @@ int EncodePointCloudToFile(const draco::PointCloud &pc, const std::string &file,
|
||||
return -1;
|
||||
}
|
||||
out_file.write(buffer.data(), buffer.size());
|
||||
printf("Encoded point cloud saved to %s (%" PRId64 " ms to encode)\n",
|
||||
printf("Encoded point cloud saved to %s (%" PRId64 " ms to encode).\n",
|
||||
file.c_str(), timer.GetInMs());
|
||||
printf("\nEncoded size = %zu bytes\n\n", buffer.size());
|
||||
return 0;
|
||||
@ -183,7 +186,7 @@ int EncodeMeshToFile(const draco::Mesh &mesh, const std::string &file,
|
||||
return -1;
|
||||
}
|
||||
out_file.write(buffer.data(), buffer.size());
|
||||
printf("Encoded mesh saved to %s (%" PRId64 " ms to encode)\n", file.c_str(),
|
||||
printf("Encoded mesh saved to %s (%" PRId64 " ms to encode).\n", file.c_str(),
|
||||
timer.GetInMs());
|
||||
printf("\nEncoded size = %zu bytes\n\n", buffer.size());
|
||||
return 0;
|
||||
@ -193,7 +196,6 @@ int EncodeMeshToFile(const draco::Mesh &mesh, const std::string &file,
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
Options options;
|
||||
draco::Encoder encoder;
|
||||
const int argc_check = argc - 1;
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
@ -329,7 +331,19 @@ int main(int argc, char **argv) {
|
||||
// Convert compression level to speed (that 0 = slowest, 10 = fastest).
|
||||
const int speed = 10 - options.compression_level;
|
||||
|
||||
draco::Encoder encoder;
|
||||
|
||||
// Setup encoder options.
|
||||
if (options.edgebreaker_method != -1)
|
||||
encoder.options().SetGlobalInt("edgebreaker_method",
|
||||
options.edgebreaker_method);
|
||||
auto iter = options.attribute_prediction_scheme.begin();
|
||||
while (iter != options.attribute_prediction_scheme.end()) {
|
||||
const draco::GeometryAttribute::Type attribute_type = iter->first;
|
||||
const int prediction_scheme = iter->second;
|
||||
encoder.SetAttributePredictionScheme(attribute_type, prediction_scheme);
|
||||
iter++;
|
||||
}
|
||||
if (options.pos_quantization_bits > 0) {
|
||||
encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION,
|
||||
options.pos_quantization_bits);
|
||||
@ -356,15 +370,16 @@ int main(int argc, char **argv) {
|
||||
PrintOptions(*pc.get(), options);
|
||||
|
||||
int ret = -1;
|
||||
if (mesh && mesh->num_faces() > 0)
|
||||
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 (ret != -1 && options.compression_level < 10) {
|
||||
printf(
|
||||
"For better compression, increase the compression level '-cl' (up to "
|
||||
"10).\n\n");
|
||||
"For better compression, increase the compression level up to '-cl 10' "
|
||||
".\n\n");
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -55,7 +55,7 @@ int DecodeMeshForUnity(char *data, unsigned int length,
|
||||
buffer.Init(data, length);
|
||||
auto type_statusor = draco::Decoder::GetEncodedGeometryType(&buffer);
|
||||
if (!type_statusor.ok()) {
|
||||
// TODO(zhafang): Use enum instead.
|
||||
// TODO(draco-eng): Use enum instead.
|
||||
return -1;
|
||||
}
|
||||
const draco::EncodedGeometryType geom_type = type_statusor.value();
|
||||
@ -82,7 +82,7 @@ int DecodeMeshForUnity(char *data, unsigned int length,
|
||||
reinterpret_cast<const int *>(face.data()), sizeof(int) * 3);
|
||||
}
|
||||
|
||||
// TODO(zhafang): Add other attributes.
|
||||
// TODO(draco-eng): Add other attributes.
|
||||
unity_mesh->position = new float[in_mesh->num_points() * 3];
|
||||
const auto pos_att =
|
||||
in_mesh->GetNamedAttribute(draco::GeometryAttribute::POSITION);
|
||||
@ -122,6 +122,11 @@ int DecodeMeshForUnity(char *data, unsigned int length,
|
||||
ReleaseUnityMesh(&unity_mesh);
|
||||
return -8;
|
||||
}
|
||||
if (color_att->num_components() < 4) {
|
||||
// If the alpha component wasn't set in the input data we should set
|
||||
// it to an opaque value.
|
||||
unity_mesh->color[i.value() * 4 + 3] = 1.f;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get texture coordinates attributes.
|
||||
|
17
testdata/deg_faces.obj
vendored
Normal file
17
testdata/deg_faces.obj
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
v 0.123 0.6514 0.000001
|
||||
v 0.342 0.1234 0.000002
|
||||
v 0.156 0.8422 0.000003
|
||||
v 0.252 0.2344 0.000004
|
||||
v 0.782 4.2515 0.000005
|
||||
v 0.127 1.2715 0.000006
|
||||
|
||||
# 6 vertices, 0 vertices normals
|
||||
|
||||
f 1 2 3
|
||||
f 3 2 6
|
||||
f 4 6 2
|
||||
f 1 1 3
|
||||
|
||||
# 4 faces, 0 coords texture
|
||||
|
||||
# End of File
|
27
testdata/multiple_isolated_triangles.obj
vendored
Normal file
27
testdata/multiple_isolated_triangles.obj
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
v 0.1 0.1 0
|
||||
v 0.2 0.1 0
|
||||
v 0.15 0.2 0
|
||||
v 1.1 1.1 0
|
||||
v 1.2 1.1 0
|
||||
v 1.15 1.2 0
|
||||
v 2.1 2.1 0
|
||||
v 2.2 2.1 0
|
||||
v 2.15 2.2 0
|
||||
v 3.1 3.1 0
|
||||
v 3.2 3.1 0
|
||||
v 3.15 3.2 0
|
||||
v 4.1 4.1 0
|
||||
v 4.2 4.1 0
|
||||
v 4.15 4.2 0
|
||||
|
||||
# 15 vertices, 0 vertices normals
|
||||
|
||||
f 3 2 1
|
||||
f 6 5 4
|
||||
f 9 8 7
|
||||
f 12 11 10
|
||||
f 15 14 13
|
||||
|
||||
# 5 faces, 0 coords texture
|
||||
|
||||
# End of File
|
47
testdata/multiple_tetrahedrons.obj
vendored
Normal file
47
testdata/multiple_tetrahedrons.obj
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
v 0.1 0.1 0
|
||||
v 0.2 0.1 0
|
||||
v 0.15 0.2 0
|
||||
v 0.15 0.1 0.1
|
||||
v 1.1 1.1 0
|
||||
v 1.2 1.1 0
|
||||
v 1.15 1.2 0
|
||||
v 1.15 1.1 0.1
|
||||
v 2.1 2.1 0
|
||||
v 2.2 2.1 0
|
||||
v 2.15 2.2 0
|
||||
v 2.15 2.1 0.1
|
||||
v 3.1 3.1 0
|
||||
v 3.2 3.1 0
|
||||
v 3.15 3.2 0
|
||||
v 3.15 3.1 0.1
|
||||
v 4.1 4.1 0
|
||||
v 4.2 4.1 0
|
||||
v 4.15 4.2 0
|
||||
v 4.15 4.1 0.1
|
||||
|
||||
# 20 vertices, 0 vertices normals
|
||||
|
||||
f 1 3 2
|
||||
f 2 3 4
|
||||
f 4 3 1
|
||||
f 1 2 4
|
||||
f 5 7 6
|
||||
f 6 7 8
|
||||
f 8 7 5
|
||||
f 5 6 8
|
||||
f 9 11 10
|
||||
f 10 11 12
|
||||
f 12 11 9
|
||||
f 9 10 12
|
||||
f 13 15 14
|
||||
f 14 15 16
|
||||
f 16 15 13
|
||||
f 13 14 16
|
||||
f 17 19 18
|
||||
f 18 19 20
|
||||
f 20 19 17
|
||||
f 17 18 20
|
||||
|
||||
# 20 faces, 0 coords texture
|
||||
|
||||
# End of File
|
Loading…
x
Reference in New Issue
Block a user