Skip to content

Commit

Permalink
Trade: implement support for tangents and bitangents in MeshData.
Browse files Browse the repository at this point in the history
Originally wanted to offload this to someone else, but then realized I
need those for generic vertex attribute definitions, which I need for
instancing, which I need now. So here it is, at the bottom of the
dependency chain.
  • Loading branch information
mosra committed Mar 16, 2020
1 parent 12c5476 commit f4bca9b
Show file tree
Hide file tree
Showing 4 changed files with 430 additions and 19 deletions.
9 changes: 9 additions & 0 deletions doc/snippets/MagnumTrade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,15 @@ Trade::MeshIndexData data{indices};
/* [MeshIndexData-usage] */
}

{
Vector3 normal;
Vector4 tangent;
/* [MeshAttribute-bitangent-from-tangent] */
Vector3 bitangent = Math::cross(normal, tangent.xyz())*tangent.w();
/* [MeshAttribute-bitangent-from-tangent] */
static_cast<void>(bitangent);
}

{
/* [MeshAttributeData-usage] */
Containers::StridedArrayView1D<const Vector3> positions;
Expand Down
105 changes: 93 additions & 12 deletions src/Magnum/Trade/MeshData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -557,27 +557,106 @@ Containers::Array<Vector3> MeshData::positions3DAsArray(const UnsignedInt id) co
return out;
}

void MeshData::normalsInto(const Containers::StridedArrayView1D<Vector3> destination, const UnsignedInt id) const {
const UnsignedInt attributeId = attributeFor(MeshAttribute::Normal, id);
CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::normalsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Normal) << "normal attributes", );
CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::normalsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), );
const MeshAttributeData& attribute = _attributes[attributeId];
CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format),
"Trade::MeshData::normalsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast<void*>(vertexFormatUnwrap(attribute._format)), );
const Containers::StridedArrayView1D<const void> attributeData = attributeDataViewInternal(attribute);
namespace {

void tangentsOrNormalsInto(const Containers::StridedArrayView1D<const void> attributeData, const Containers::StridedArrayView1D<Vector3> destination, VertexFormat format) {
const auto destination3f = Containers::arrayCast<2, Float>(destination);

if(attribute._format == VertexFormat::Vector3)
if(format == VertexFormat::Vector3)
Utility::copy(Containers::arrayCast<const Vector3>(attributeData), destination);
else if(attribute._format == VertexFormat::Vector3h)
else if(format == VertexFormat::Vector3h)
Math::unpackHalfInto(Containers::arrayCast<2, const UnsignedShort>(attributeData, 3), destination3f);
else if(attribute._format == VertexFormat::Vector3bNormalized)
else if(format == VertexFormat::Vector3bNormalized)
Math::unpackInto(Containers::arrayCast<2, const Byte>(attributeData, 3), destination3f);
else if(attribute._format == VertexFormat::Vector3sNormalized)
else if(format == VertexFormat::Vector3sNormalized)
Math::unpackInto(Containers::arrayCast<2, const Short>(attributeData, 3), destination3f);
else CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
}

}

void MeshData::tangentsInto(const Containers::StridedArrayView1D<Vector3> destination, const UnsignedInt id) const {
const UnsignedInt attributeId = attributeFor(MeshAttribute::Tangent, id);
CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::tangentsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Tangent) << "tangent attributes", );
CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::tangentsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), );
const MeshAttributeData& attribute = _attributes[attributeId];
CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format),
"Trade::MeshData::tangentsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast<void*>(vertexFormatUnwrap(attribute._format)), );

/* If the tangent is four-component, ignore the last component; otherwise
copy/unpack given format directly */
VertexFormat format;
if(attribute._format == VertexFormat::Vector4)
format = VertexFormat::Vector3;
else if(attribute._format == VertexFormat::Vector4h)
format = VertexFormat::Vector3h;
else if(attribute._format == VertexFormat::Vector4ubNormalized)
format = VertexFormat::Vector3ubNormalized;
else if(attribute._format == VertexFormat::Vector4usNormalized)
format = VertexFormat::Vector3usNormalized;
else format = attribute._format;
tangentsOrNormalsInto(attributeDataViewInternal(attribute), destination, format);
}

Containers::Array<Vector3> MeshData::tangentsAsArray(const UnsignedInt id) const {
Containers::Array<Vector3> out{_vertexCount};
tangentsInto(out, id);
return out;
}

void MeshData::bitangentSignsInto(const Containers::StridedArrayView1D<Float> destination, const UnsignedInt id) const {
const UnsignedInt attributeId = attributeFor(MeshAttribute::Tangent, id);
CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::bitangentSignsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Tangent) << "tangent attributes", );
CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::bitangentSignsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), );
const MeshAttributeData& attribute = _attributes[attributeId];
CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format),
"Trade::MeshData::bitangentSignsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast<void*>(vertexFormatUnwrap(attribute._format)), );
const Containers::StridedArrayView1D<const void> attributeData = attributeDataViewInternal(attribute);
const auto destination1f = Containers::arrayCast<2, Float>(destination);

if(attribute._format == VertexFormat::Vector4)
Utility::copy(Containers::arrayCast<2, const Float>(attributeData, 4).transposed<0, 1>()[3], destination);
else if(attribute._format == VertexFormat::Vector4h)
Math::unpackHalfInto(Containers::arrayCast<2, const UnsignedShort>(attributeData, 4).suffix({0, 3}), destination1f);
else if(attribute._format == VertexFormat::Vector4bNormalized)
Math::unpackInto(Containers::arrayCast<2, const Byte>(attributeData, 4).suffix({0, 3}), destination1f);
else if(attribute._format == VertexFormat::Vector4sNormalized)
Math::unpackInto(Containers::arrayCast<2, const Short>(attributeData, 4).suffix({0, 3}), destination1f);
else CORRADE_ASSERT(false, "Trade::MeshData::bitangentSignsInto(): expected four-component tangents, but got" << attribute._format, );
}

Containers::Array<Float> MeshData::bitangentSignsAsArray(const UnsignedInt id) const {
Containers::Array<Float> out{_vertexCount};
bitangentSignsInto(out, id);
return out;
}

void MeshData::bitangentsInto(const Containers::StridedArrayView1D<Vector3> destination, const UnsignedInt id) const {
const UnsignedInt attributeId = attributeFor(MeshAttribute::Bitangent, id);
CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::bitangentsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Bitangent) << "bitangent attributes", );
CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::bitangentsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), );
const MeshAttributeData& attribute = _attributes[attributeId];
CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format),
"Trade::MeshData::bitangentsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast<void*>(vertexFormatUnwrap(attribute._format)), );
tangentsOrNormalsInto(attributeDataViewInternal(attribute), destination, attribute._format);
}

Containers::Array<Vector3> MeshData::bitangentsAsArray(const UnsignedInt id) const {
Containers::Array<Vector3> out{_vertexCount};
bitangentsInto(out, id);
return out;
}

void MeshData::normalsInto(const Containers::StridedArrayView1D<Vector3> destination, const UnsignedInt id) const {
const UnsignedInt attributeId = attributeFor(MeshAttribute::Normal, id);
CORRADE_ASSERT(attributeId != ~UnsignedInt{}, "Trade::MeshData::normalsInto(): index" << id << "out of range for" << attributeCount(MeshAttribute::Normal) << "normal attributes", );
CORRADE_ASSERT(destination.size() == _vertexCount, "Trade::MeshData::normalsInto(): expected a view with" << _vertexCount << "elements but got" << destination.size(), );
const MeshAttributeData& attribute = _attributes[attributeId];
CORRADE_ASSERT(!isVertexFormatImplementationSpecific(attribute._format),
"Trade::MeshData::normalsInto(): can't extract data out of an implementation-specific vertex format" << reinterpret_cast<void*>(vertexFormatUnwrap(attribute._format)), );
tangentsOrNormalsInto(attributeDataViewInternal(attribute), destination, attribute._format);
}

Containers::Array<Vector3> MeshData::normalsAsArray(const UnsignedInt id) const {
Containers::Array<Vector3> out{_vertexCount};
normalsInto(out, id);
Expand Down Expand Up @@ -704,6 +783,8 @@ Debug& operator<<(Debug& debug, const MeshAttribute value) {
/* LCOV_EXCL_START */
#define _c(value) case MeshAttribute::value: return debug << "::" << Debug::nospace << #value;
_c(Position)
_c(Tangent)
_c(Bitangent)
_c(Normal)
_c(TextureCoordinates)
_c(Color)
Expand Down
141 changes: 134 additions & 7 deletions src/Magnum/Trade/MeshData.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ enum class MeshAttribute: UnsignedShort {
*/
Position = 1,

/**
* Tangent, optionally including bitangent sign. In the first case the type
* is usually @ref VertexFormat::Vector3, but can be also
* @ref VertexFormat::Vector3h, @ref VertexFormat::Vector3bNormalized or
* @ref VertexFormat::Vector3sNormalized; in the second case the type is
* @ref VertexFormat::Vector4 (or @ref VertexFormat::Vector4h,
* @ref VertexFormat::Vector4bNormalized,
* @ref VertexFormat::Vector4sNormalized) and the fourth component is a
* sign value (@cpp -1.0f @ce or @cpp +1.0f @ce) defining handedness of the
* tangent basis. Reconstruct the @ref MeshAttribute::Bitangent can be then
* done like this:
*
* @snippet MagnumTrade.cpp MeshAttribute-bitangent-from-tangent
*
* Corresponds to @ref Shaders::Generic::Tangent.
* @see @ref MeshData::tangentsAsArray(),
* @ref MeshData::bitangentSignsAsArray()
*/
Tangent,

/**
* Bitangent. Type is usually @ref VertexFormat::Vector3, but can be also
* @ref VertexFormat::Vector3h, @ref VertexFormat::Vector3bNormalized or
* @ref VertexFormat::Vector3sNormalized. For better storage efficiency,
* the bitangent can be also reconstructed from the normal and tangent, see
* @ref MeshAttribute::Tangent for more information.
* @see @ref MeshData::bitangentsAsArray()
*/
Bitangent,

/**
* Normal. Type is usually @ref VertexFormat::Vector3, but can be also
* @ref VertexFormat::Vector3h. @ref VertexFormat::Vector3bNormalized or
Expand Down Expand Up @@ -563,7 +593,8 @@ the @ref Primitives library.
@section Trade-MeshData-usage Basic usage
The simplest usage is through the convenience functions @ref positions2DAsArray(),
@ref positions3DAsArray(), @ref normalsAsArray(), @ref textureCoordinates2DAsArray()
@ref positions3DAsArray(), @ref tangentsAsArray(), @ref bitangentsAsArray(),
@ref normalsAsArray(), @ref tangentsAsArray(), @ref textureCoordinates2DAsArray()
and @ref colorsAsArray(). Each of these takes an index (as there can be
multiple sets of texture coordinates, for example) and you're expected to check
for attribute presence first with either @ref hasAttribute() or
Expand Down Expand Up @@ -1293,10 +1324,12 @@ class MAGNUM_TRADE_EXPORT MeshData {
* case you need to use the overload below by using @cpp T[] @ce
* instead of @cpp T @ce. You can also use the non-templated
* @ref positions2DAsArray(), @ref positions3DAsArray(),
* @ref normalsAsArray(), @ref textureCoordinates2DAsArray() and
* @ref colorsAsArray() accessors to get common attributes converted to
* usual types, but note that these operations involve extra allocation
* and data conversion.
* @ref tangentsAsArray(), @ref bitangentSignsAsArray(),
* @ref bitangentsAsArray(), @ref normalsAsArray(),
* @ref textureCoordinates2DAsArray() and @ref colorsAsArray()
* accessors to get common attributes converted to usual types, but
* note that these operations involve extra allocation and data
* conversion.
* @see @ref attribute(MeshAttribute, UnsignedInt) const,
* @ref mutableAttribute(MeshAttribute, UnsignedInt),
* @ref isVertexFormatImplementationSpecific(),
Expand Down Expand Up @@ -1483,6 +1516,90 @@ class MAGNUM_TRADE_EXPORT MeshData {
*/
void positions3DInto(Containers::StridedArrayView1D<Vector3> destination, UnsignedInt id = 0) const;

/**
* @brief Tangents as 3D float vectors
*
* Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const
* with @ref MeshAttribute::Tangent as the first argument. Converts the
* tangent array from an arbitrary underlying type and returns it in a
* newly-allocated array. Expects that the vertex format is *not*
* implementation-specific, in that case you can only access the
* attribute via the typeless @ref attribute(MeshAttribute, UnsignedInt) const.
*
* If the tangents contain a fourth component with bitangent direction,
* it's ignored here --- use @ref bitangentSignsAsArray() to get those
* instead. You can also use @ref tangentsInto() together with
* @ref bitangentSignsInto() to put them both in a single array.
* @see @ref bitangentsAsArray(), @ref normalsAsArray(),
* @ref attributeFormat(),
* @ref isVertexFormatImplementationSpecific()
*/
Containers::Array<Vector3> tangentsAsArray(UnsignedInt id = 0) const;

/**
* @brief Tangents as 3D float vectors into a pre-allocated view
*
* Like @ref tangentsAsArray(), but puts the result into @p destination
* instead of allocating a new array. Expects that @p destination is
* sized to contain exactly all data.
* @see @ref vertexCount()
*/
void tangentsInto(Containers::StridedArrayView1D<Vector3> destination, UnsignedInt id = 0) const;

/**
* @brief Bitangent signs as floats
*
* Counterpart to @ref tangentsAsArray() returning value of the fourth
* component. Expects that the type of @ref MeshAttribute::Tangent is
* four-component. You can also use @ref tangentsInto() together with
* @ref bitangentSignsInto() to put them both in a single array.
* @see @ref tangentsAsArray(), @ref bitangentsAsArray(),
* @ref normalsAsArray(), @ref attributeFormat(),
* @ref isVertexFormatImplementationSpecific()
*/
Containers::Array<Float> bitangentSignsAsArray(UnsignedInt id = 0) const;

/**
* @brief Bitangent signs as floats into a pre-allocated view
*
* Like @ref bitangentsAsArray(), but puts the result into
* @p destination instead of allocating a new array. Expects that
* @p destination is sized to contain exactly all data.
* @see @ref vertexCount()
*/
void bitangentSignsInto(Containers::StridedArrayView1D<Float> destination, UnsignedInt id = 0) const;

/**
* @brief Bitangents as 3D float vectors
*
* Convenience alternative to @ref attribute(MeshAttribute, UnsignedInt) const
* with @ref MeshAttribute::Bitangent as the first argument. Converts
* the bitangent array from an arbitrary underlying type and returns it
* in a newly-allocated array. Expects that the vertex format is *not*
* implementation-specific, in that case you can only access the
* attribute via the typeless @ref attribute(MeshAttribute, UnsignedInt) const.
*
* Note that in some cases the bitangents aren't provided directly but
* calculated from normals and four-component tangents. In that case
* you'll need to get bitangent signs via @ref bitangentSignsAsArray()
* and calculate the bitangents as shown in the documentation of
* @ref MeshAttribute::Tangent.
* @see @ref bitangentsInto(), @ref tangentsAsArray(),
* @ref normalsAsArray(), @ref attributeFormat(),
* @ref isVertexFormatImplementationSpecific()
*/
Containers::Array<Vector3> bitangentsAsArray(UnsignedInt id = 0) const;

/**
* @brief Bitangents as 3D float vectors into a pre-allocated view
*
* Like @ref bitangentsAsArray(), but puts the result into
* @p destination instead of allocating a new array. Expects that
* @p destination is sized to contain exactly all data.
* @see @ref vertexCount()
*/
void bitangentsInto(Containers::StridedArrayView1D<Vector3> destination, UnsignedInt id = 0) const;

/**
* @brief Normals as 3D float vectors
*
Expand All @@ -1492,7 +1609,8 @@ class MAGNUM_TRADE_EXPORT MeshData {
* newly-allocated array. Expects that the vertex format is *not*
* implementation-specific, in that case you can only access the
* attribute via the typeless @ref attribute(MeshAttribute, UnsignedInt) const.
* @see @ref normalsInto(), @ref attributeFormat(),
* @see @ref normalsInto(), @ref tangentsAsArray(),
* @ref bitangentsAsArray(), @ref attributeFormat(),
* @ref isVertexFormatImplementationSpecific()
*/
Containers::Array<Vector3> normalsAsArray(UnsignedInt id = 0) const;
Expand Down Expand Up @@ -1862,7 +1980,16 @@ namespace Implementation {
format == VertexFormat::Vector3usNormalized ||
format == VertexFormat::Vector3s ||
format == VertexFormat::Vector3sNormalized)) ||
(name == MeshAttribute::Normal &&
(name == MeshAttribute::Tangent &&
(format == VertexFormat::Vector3 ||
format == VertexFormat::Vector3h ||
format == VertexFormat::Vector3bNormalized ||
format == VertexFormat::Vector3sNormalized ||
format == VertexFormat::Vector4 ||
format == VertexFormat::Vector4h ||
format == VertexFormat::Vector4bNormalized ||
format == VertexFormat::Vector4sNormalized)) ||
((name == MeshAttribute::Bitangent || name == MeshAttribute::Normal) &&
(format == VertexFormat::Vector3 ||
format == VertexFormat::Vector3h ||
format == VertexFormat::Vector3bNormalized ||
Expand Down
Loading

0 comments on commit f4bca9b

Please sign in to comment.