From acd652938bbbd4a026f8a98bd492ca9a28781271 Mon Sep 17 00:00:00 2001 From: Filyus Date: Sun, 4 Oct 2026 13:50:29 +0500 Subject: [PATCH] Build geometric normal prediction only over octahedron transforms Integer normals that are not quantized keep the wrap transform, and below speed 4 the encoder selects MESH_PREDICTION_GEOMETRIC_NORMAL for them. MeshPredictionSchemeEncoderFactory builds that scheme over whatever transform it is given, and the scheme asks the wrap transform for quantization bits, which it answers with a stub returning -1 behind DRACO_DCHECK(false). Debug builds assert. Release builds predict from an uninitialized octahedron toolbox and write a stream that decodes to different normals, with every step reporting success. The decoder factory builds geometric normal prediction only for the normal octahedron transforms. The encoder factory now does the same and returns nullptr otherwise, so the caller falls back to delta coding as it does for any scheme that cannot be built. Quantized normals are unaffected: draco_encoder writes the same bytes as before for cube_att.obj, test_nm.obj, sphere.obj, bun_zipper.ply and a point cloud with normals at -cl 0, 1, 2, 3, 4, 7 and 10. TestIntegerNormalsAtSpeedZeroRoundTrip encodes a 5x5 grid with int32 normals in [-3, 3] at speeds 0/0 and compares the decoded normals with the input. Before this change they come back as unrelated values. --- .../prediction_scheme_encoder_factory.h | 10 ++- src/draco/compression/encode_test.cc | 71 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h b/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h index 11db5a62e..bc3148990 100644 --- a/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h +++ b/src/draco/compression/attributes/prediction_schemes/prediction_scheme_encoder_factory.h @@ -67,7 +67,15 @@ struct MeshPredictionSchemeEncoderFactory { mesh_data)); } #ifdef DRACO_NORMAL_ENCODING_SUPPORTED - else if (method == MESH_PREDICTION_GEOMETRIC_NORMAL) { + // Geometric normal prediction works on octahedral coordinates, which only + // the normal octahedron transforms provide. Integer normals that are not + // quantized use the wrap transform, which has no quantization bits for the + // predictor. For them the factory returns nullptr and the caller falls back + // to delta coding, as it does for any scheme it cannot build. + else if (method == MESH_PREDICTION_GEOMETRIC_NORMAL && + (transform.GetType() == + PREDICTION_TRANSFORM_NORMAL_OCTAHEDRON_CANONICALIZED || + transform.GetType() == PREDICTION_TRANSFORM_NORMAL_OCTAHEDRON)) { return std::unique_ptr>( new MeshPredictionSchemeGeometricNormalEncoder( diff --git a/src/draco/compression/encode_test.cc b/src/draco/compression/encode_test.cc index 461dddef7..4df86127e 100644 --- a/src/draco/compression/encode_test.cc +++ b/src/draco/compression/encode_test.cc @@ -15,6 +15,8 @@ #include "draco/compression/encode.h" +#include +#include #include #include #include @@ -637,4 +639,73 @@ TEST_F(EncodeTest, TestDracoCompressionOptionsGridQuantizationWithOffset) { } #endif // DRACO_TRANSCODER_SUPPORTED +// Integer normals at the lowest speed. The encoder selects geometric normal +// prediction for normals, and these have no octahedral coordinates. They must +// still round-trip. +TEST_F(EncodeTest, TestIntegerNormalsAtSpeedZeroRoundTrip) { + draco::Mesh mesh; + mesh.set_num_points(25); + draco::GeometryAttribute pos; + pos.Init(draco::GeometryAttribute::POSITION, nullptr, 3, draco::DT_FLOAT32, + false, sizeof(float) * 3, 0); + const int pos_id = mesh.AddAttribute(pos, true, 25); + draco::GeometryAttribute nrm; + nrm.Init(draco::GeometryAttribute::NORMAL, nullptr, 3, draco::DT_INT32, false, + sizeof(int32_t) * 3, 0); + const int nrm_id = mesh.AddAttribute(nrm, true, 25); + for (int y = 0; y < 5; ++y) { + for (int x = 0; x < 5; ++x) { + const int p = y * 5 + x; + const float position[3] = {static_cast(x), static_cast(y), + ((x * y) % 3) * 0.25f}; + const int32_t normal[3] = {(p % 7) - 3, ((p + 1) % 7) - 3, + ((p + 2) % 7) - 3}; + mesh.attribute(pos_id)->SetAttributeValue(draco::AttributeValueIndex(p), + position); + mesh.attribute(nrm_id)->SetAttributeValue(draco::AttributeValueIndex(p), + normal); + } + } + for (int y = 0; y < 4; ++y) { + for (int x = 0; x < 4; ++x) { + const int i = y * 5 + x; + mesh.AddFace({draco::PointIndex(i), draco::PointIndex(i + 1), + draco::PointIndex(i + 5)}); + mesh.AddFace({draco::PointIndex(i + 1), draco::PointIndex(i + 6), + draco::PointIndex(i + 5)}); + } + } + + draco::Encoder encoder; + encoder.SetSpeedOptions(0, 0); + encoder.SetAttributeQuantization(draco::GeometryAttribute::POSITION, 11); + draco::EncoderBuffer buffer; + DRACO_ASSERT_OK(encoder.EncodeMeshToBuffer(mesh, &buffer)); + + draco::DecoderBuffer in; + in.Init(buffer.data(), buffer.size()); + draco::Decoder decoder; + auto decoded = decoder.DecodeMeshFromBuffer(&in); + ASSERT_TRUE(decoded.ok()) << decoded.status().error_msg_string(); + const auto &out = *decoded.value(); + const draco::PointAttribute *out_nrm = + out.GetNamedAttribute(draco::GeometryAttribute::NORMAL); + ASSERT_NE(out_nrm, nullptr); + // The encoder reorders points, so the normals are compared as sorted lists. + std::vector> want, got; + for (int p = 0; p < 25; ++p) { + std::array v; + mesh.attribute(nrm_id)->GetValue(draco::AttributeValueIndex(p), v.data()); + want.push_back(v); + } + for (draco::PointIndex p(0); p < out.num_points(); ++p) { + std::array v; + out_nrm->GetMappedValue(p, v.data()); + got.push_back(v); + } + std::sort(want.begin(), want.end()); + std::sort(got.begin(), got.end()); + ASSERT_EQ(got, want); +} + } // namespace