// 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. // // Utilities for converting unit vectors to octahedral coordinates and back. // For more details about octahedral coordinates, see for example Cigolle // et al.'14 “A Survey of Efficient Representations for Independent Unit // Vectors”. // // In short this is motivated by an octahedron inscribed into a sphere. The // direction of the normal vector can be defined by a point on the octahedron. // On the right hemisphere (x > 0) this point is projected onto the x = 0 plane, // that is, the right side of the octahedron forms a diamond like shape. The // left side of the octahedron is also projected onto the x = 0 plane, however, // in this case we flap the triangles of the diamond outward. Afterwards we // shift the resulting square such that all values are positive. // // Important values in this file: // * q: number of quantization bits // * max_quantized_value: the max value representable with q bits (odd) // * max_value: max value of the diamond = max_quantized_value - 1 (even) // * center_value: center of the diamond after shift // // Note that the parameter space is somewhat periodic, e.g. (0, 0) == // (max_value, max_value), which is also why the diamond is one smaller than the // maximal representable value in order to have an odd range of values. #ifndef DRACO_COMPRESSION_ATTRIBUTES_NORMAL_COMPRESSION_UTILS_H_ #define DRACO_COMPRESSION_ATTRIBUTES_NORMAL_COMPRESSION_UTILS_H_ #include #include #include #include "draco/core/macros.h" namespace draco { class OctahedronToolBox { public: OctahedronToolBox() : quantization_bits_(-1), max_quantized_value_(-1), max_value_(-1), dequantization_scale_(1.f), center_value_(-1) {} bool SetQuantizationBits(int32_t q) { if (q < 2 || q > 30) { return false; } quantization_bits_ = q; max_quantized_value_ = (1 << quantization_bits_) - 1; max_value_ = max_quantized_value_ - 1; dequantization_scale_ = 2.f / max_value_; center_value_ = max_value_ / 2; return true; } bool IsInitialized() const { return quantization_bits_ != -1; } // Convert all edge points in the top left and bottom right quadrants to // their corresponding position in the bottom left and top right quadrants. // Convert all corner edge points to the top right corner. inline void CanonicalizeOctahedralCoords(int32_t s, int32_t t, int32_t *out_s, int32_t *out_t) const { if ((s == 0 && t == 0) || (s == 0 && t == max_value_) || (s == max_value_ && t == 0)) { s = max_value_; t = max_value_; } else if (s == 0 && t > center_value_) { t = center_value_ - (t - center_value_); } else if (s == max_value_ && t < center_value_) { t = center_value_ + (center_value_ - t); } else if (t == max_value_ && s < center_value_) { s = center_value_ + (center_value_ - s); } else if (t == 0 && s > center_value_) { s = center_value_ - (s - center_value_); } *out_s = s; *out_t = t; } // Converts an integer vector to octahedral coordinates. // Precondition: |int_vec| abs sum must equal center value. inline void IntegerVectorToQuantizedOctahedralCoords(const int32_t *int_vec, int32_t *out_s, int32_t *out_t) const { DRACO_DCHECK_EQ( std::abs(int_vec[0]) + std::abs(int_vec[1]) + std::abs(int_vec[2]), center_value_); int32_t s, t; if (int_vec[0] >= 0) { // Right hemisphere. s = (int_vec[1] + center_value_); t = (int_vec[2] + center_value_); } else { // Left hemisphere. if (int_vec[1] < 0) { s = std::abs(int_vec[2]); } else { s = (max_value_ - std::abs(int_vec[2])); } if (int_vec[2] < 0) { t = std::abs(int_vec[1]); } else { t = (max_value_ - std::abs(int_vec[1])); } } CanonicalizeOctahedralCoords(s, t, out_s, out_t); } template void FloatVectorToQuantizedOctahedralCoords(const T *vector, int32_t *out_s, int32_t *out_t) const { const double abs_sum = std::abs(static_cast(vector[0])) + std::abs(static_cast(vector[1])) + std::abs(static_cast(vector[2])); // Adjust values such that abs sum equals 1. double scaled_vector[3]; if (abs_sum > 1e-6) { // Scale needed to project the vector to the surface of an octahedron. const double scale = 1.0 / abs_sum; scaled_vector[0] = vector[0] * scale; scaled_vector[1] = vector[1] * scale; scaled_vector[2] = vector[2] * scale; } else { scaled_vector[0] = 1.0; scaled_vector[1] = 0; scaled_vector[2] = 0; } // Scale vector such that the sum equals the center value. int32_t int_vec[3]; int_vec[0] = static_cast(floor(scaled_vector[0] * center_value_ + 0.5)); int_vec[1] = static_cast(floor(scaled_vector[1] * center_value_ + 0.5)); // Make sure the sum is exactly the center value. int_vec[2] = center_value_ - std::abs(int_vec[0]) - std::abs(int_vec[1]); if (int_vec[2] < 0) { // If the sum of first two coordinates is too large, we need to decrease // the length of one of the coordinates. if (int_vec[1] > 0) { int_vec[1] += int_vec[2]; } else { int_vec[1] -= int_vec[2]; } int_vec[2] = 0; } // Take care of the sign. if (scaled_vector[2] < 0) { int_vec[2] *= -1; } IntegerVectorToQuantizedOctahedralCoords(int_vec, out_s, out_t); } // Normalize |vec| such that its abs sum is equal to the center value; template void CanonicalizeIntegerVector(T *vec) const { static_assert(std::is_integral::value, "T must be an integral type."); static_assert(std::is_signed::value, "T must be a signed type."); const int64_t abs_sum = static_cast(std::abs(vec[0])) + static_cast(std::abs(vec[1])) + static_cast(std::abs(vec[2])); if (abs_sum == 0) { vec[0] = center_value_; // vec[1] == v[2] == 0 } else { vec[0] = (static_cast(vec[0]) * static_cast(center_value_)) / abs_sum; vec[1] = (static_cast(vec[1]) * static_cast(center_value_)) / abs_sum; if (vec[2] >= 0) { vec[2] = center_value_ - std::abs(vec[0]) - std::abs(vec[1]); } else { vec[2] = -(center_value_ - std::abs(vec[0]) - std::abs(vec[1])); } } } inline void QuantizedOctahedralCoordsToUnitVector(int32_t in_s, int32_t in_t, float *out_vector) const { OctahedralCoordsToUnitVector(in_s * dequantization_scale_ - 1.f, in_t * dequantization_scale_ - 1.f, out_vector); } // |s| and |t| are expected to be signed values. inline bool IsInDiamond(const int32_t &s, const int32_t &t) const { // Expect center already at origin. DRACO_DCHECK_LE(s, center_value_); 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_; } void InvertDiamond(int32_t *s, int32_t *t) const { // Expect center already at origin. DRACO_DCHECK_LE(*s, center_value_); DRACO_DCHECK_LE(*t, center_value_); DRACO_DCHECK_GE(*s, -center_value_); DRACO_DCHECK_GE(*t, -center_value_); int32_t sign_s = 0; int32_t sign_t = 0; if (*s >= 0 && *t >= 0) { sign_s = 1; sign_t = 1; } else if (*s <= 0 && *t <= 0) { sign_s = -1; sign_t = -1; } else { sign_s = (*s > 0) ? 1 : -1; 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; if (sign_s * sign_t >= 0) { int32_t temp = *s; *s = -*t; *t = -temp; } else { std::swap(*s, *t); } *s = (*s + corner_point_s) / 2; *t = (*t + corner_point_t) / 2; } void InvertDirection(int32_t *s, int32_t *t) const { // Expect center already at origin. DRACO_DCHECK_LE(*s, center_value_); DRACO_DCHECK_LE(*t, center_value_); DRACO_DCHECK_GE(*s, -center_value_); DRACO_DCHECK_GE(*t, -center_value_); *s *= -1; *t *= -1; this->InvertDiamond(s, t); } // For correction values. int32_t ModMax(int32_t x) const { if (x > this->center_value()) { return x - this->max_quantized_value(); } if (x < -this->center_value()) { return x + this->max_quantized_value(); } return x; } // For correction values. int32_t MakePositive(int32_t x) const { DRACO_DCHECK_LE(x, this->center_value() * 2); if (x < 0) { return x + this->max_quantized_value(); } return x; } int32_t quantization_bits() const { return quantization_bits_; } int32_t max_quantized_value() const { return max_quantized_value_; } int32_t max_value() const { return max_value_; } int32_t center_value() const { return center_value_; } private: inline void OctahedralCoordsToUnitVector(float in_s_scaled, float in_t_scaled, float *out_vector) const { // Background about the encoding: // A normal is encoded in a normalized space depicted below. The // encoding correponds to an octahedron that is unwrapped to a 2D plane. // During encoding, a normal is projected to the surface of the octahedron // and the projection is then unwrapped to the 2D plane. Decoding is the // reverse of this process. // All points in the central diamond are located on triangles on the // right "hemisphere" of the octahedron while all points outside of the // diamond are on the left hemisphere (basically, they would have to be // wrapped along the diagonal edges to form the octahedron). The central // point corresponds to the right most vertex of the octahedron and all // corners of the plane correspond to the left most vertex of the // octahedron. // // t // ^ *-----*-----* // | | /|\ | // | / | \ | // | / | \ | // | / | \ | // *-----*---- * // | \ | / | // | \ | / | // | \ | / | // | \|/ | // *-----*-----* --> s // Note that the input |in_s_scaled| and |in_t_scaled| are already scaled to // <-1, 1> range. This way, the central point is at coordinate (0, 0). float y = in_s_scaled; float z = in_t_scaled; // 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); // |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 // diamond (right hemisphere) and negative for all points outside the // diamond (left hemisphere). For all points on the left hemisphere we need // to update their (y, z) coordinates to account for the wrapping along // the edges of the diamond. float x_offset = -x; x_offset = x_offset < 0 ? 0 : x_offset; // This will do nothing for the points on the right hemisphere but it will // mirror the (y, z) location along the nearest diagonal edge of the // diamond. y += y < 0 ? x_offset : -x_offset; z += z < 0 ? x_offset : -x_offset; // Normalize the computed vector. const float norm_squared = x * x + y * y + z * z; if (norm_squared < 1e-6) { out_vector[0] = 0; out_vector[1] = 0; out_vector[2] = 0; } else { const float d = 1.0f / std::sqrt(norm_squared); out_vector[0] = x * d; out_vector[1] = y * d; out_vector[2] = z * d; } } int32_t quantization_bits_; int32_t max_quantized_value_; int32_t max_value_; float dequantization_scale_; int32_t center_value_; }; } // namespace draco #endif // DRACO_COMPRESSION_ATTRIBUTES_NORMAL_COMPRESSION_UTILS_H_