From edb0cba58525ea9c3c370eb6ae9ecff1ae524cca Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Fri, 22 Feb 2019 11:26:37 -0800 Subject: [PATCH] Introduce SurfaceOrientation helper class. This moves our existing VertexBuffer utility into a new "geometry" library and adds new functionality for computing tangents based on UV's. The UV-based method comes from Eric Lengyel. We considered mikktspace (which thankfully has a zlib-style license) but it would require re-indexing via meshoptimizer and is therefore a bit heavyweight. The new library has no dependencies and will add only 7 KB to Filament's Android aar file. Fixes #858 and preps for #528. --- CMakeLists.txt | 1 + README.md | 1 + android/filament-android/CMakeLists.txt | 5 + filament/CMakeLists.txt | 1 + filament/include/filament/VertexBuffer.h | 7 +- filament/src/VertexBuffer.cpp | 62 +--- libs/geometry/CMakeLists.txt | 48 +++ .../include/geometry/SurfaceOrientation.h | 118 +++++++ libs/geometry/src/SurfaceOrientation.cpp | 290 ++++++++++++++++++ 9 files changed, 480 insertions(+), 53 deletions(-) create mode 100644 libs/geometry/CMakeLists.txt create mode 100644 libs/geometry/include/geometry/SurfaceOrientation.h create mode 100644 libs/geometry/src/SurfaceOrientation.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a99b7825cb58..4680f88b7115 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -339,6 +339,7 @@ add_subdirectory(${EXTERNAL}/libgtest/tnt) add_subdirectory(${LIBRARIES}/filabridge) add_subdirectory(${LIBRARIES}/filaflat) add_subdirectory(${LIBRARIES}/filameshio) +add_subdirectory(${LIBRARIES}/geometry) add_subdirectory(${LIBRARIES}/image) add_subdirectory(${LIBRARIES}/math) add_subdirectory(${LIBRARIES}/utils) diff --git a/README.md b/README.md index b43840e64ddc..a30de01e316d 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ and tools. - `filagui`: Helper library for [Dear ImGui](https://github.com/ocornut/imgui) - `filamat`: Material generation library - `filameshio`: Tiny mesh parsing library (see also `tools/filamesh`) + - `geometry`: Mesh-related utilities - `image`: Image filtering and simple transforms - `imageio`: Image file reading / writing, only intended for internal use - `math`: Math library diff --git a/android/filament-android/CMakeLists.txt b/android/filament-android/CMakeLists.txt index c6f703ee633c..e923aac8bfff 100644 --- a/android/filament-android/CMakeLists.txt +++ b/android/filament-android/CMakeLists.txt @@ -14,6 +14,10 @@ add_library(filaflat STATIC IMPORTED) set_target_properties(filaflat PROPERTIES IMPORTED_LOCATION ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libfilaflat.a) +add_library(geometry STATIC IMPORTED) +set_target_properties(geometry PROPERTIES IMPORTED_LOCATION + ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libgeometry.a) + add_library(filabridge STATIC IMPORTED) set_target_properties(filabridge PROPERTIES IMPORTED_LOCATION ${FILAMENT_DIR}/lib/${ANDROID_ABI}/libfilabridge.a) @@ -74,6 +78,7 @@ target_link_libraries(filament-jni filament filaflat filabridge + geometry utils log GLESv3 diff --git a/filament/CMakeLists.txt b/filament/CMakeLists.txt index 0572d53d83e5..f74ac3f9db44 100644 --- a/filament/CMakeLists.txt +++ b/filament/CMakeLists.txt @@ -347,6 +347,7 @@ endif() target_link_libraries(${TARGET} PUBLIC math) target_link_libraries(${TARGET} PUBLIC utils) +target_link_libraries(${TARGET} PUBLIC geometry) # TODO: remove this dependency after deprecating VertexBuffer::populateTangentQuaternions target_link_libraries(${TARGET} PUBLIC filaflat) target_link_libraries(${TARGET} PUBLIC filabridge) target_link_libraries(${TARGET} PUBLIC image_headers) diff --git a/filament/include/filament/VertexBuffer.h b/filament/include/filament/VertexBuffer.h index 9db1967e7755..4ebc5ac2c1e5 100644 --- a/filament/include/filament/VertexBuffer.h +++ b/filament/include/filament/VertexBuffer.h @@ -133,6 +133,9 @@ class UTILS_PUBLIC VertexBuffer : public FilamentAPI { * Convenience function that consumes normal vectors (and, optionally, tangent vectors) and * produces quaternions that can be passed into a TANGENTS buffer. * + * We may deprecate this method in the future. See also filament::geometry::SurfaceOrientation, + * which has additional capabilities. + * * The given output buffer must be preallocated with at least quatCount * outStride bytes. * * Normals are required but tangents are optional, in which case this function tries to generate @@ -141,10 +144,6 @@ class UTILS_PUBLIC VertexBuffer : public FilamentAPI { * If supplied, the tangent vectors should be unit length and should be orthogonal to the * normals. The w component of the tangent is a sign (-1 or +1) indicating handedness of the * basis. - * - * Note that some applications and file formats (e.g. Blender and glTF) use mikktspace, which - * consumes full topology information and produces an unindexed mesh, so it cannot be used here. - * This function exists for simple use cases only. */ static void populateTangentQuaternions(const QuatTangentContext& ctx); }; diff --git a/filament/src/VertexBuffer.cpp b/filament/src/VertexBuffer.cpp index 5866756a6a9a..74fbd007ecbe 100644 --- a/filament/src/VertexBuffer.cpp +++ b/filament/src/VertexBuffer.cpp @@ -20,6 +20,8 @@ #include "FilamentAPI-impl.h" +#include + #include #include #include @@ -206,65 +208,27 @@ void VertexBuffer::setBufferAt(Engine& engine, uint8_t bufferIndex, } void VertexBuffer::populateTangentQuaternions(const QuatTangentContext& ctx) { - if (!ASSERT_PRECONDITION_NON_FATAL(ctx.normals, "Normals must be provided")) { - return; - } + auto quats = geometry::SurfaceOrientation::Builder() + .vertexCount(ctx.quatCount) + .normals(ctx.normals) + .normalStride(ctx.normalsStride) + .tangents(ctx.tangents) + .tangentStride(ctx.tangentsStride) + .build(); - // Define a small lambda that converts fp32 into the desired output format. - size_t outStride = ctx.outStride; - void (*writeQuat)(quatf, uint8_t*); switch (ctx.quatType) { case HALF4: - writeQuat = [] (quatf inquat, uint8_t* outquat) { - *((quath*) outquat) = quath(inquat); - }; - outStride = outStride ? outStride : sizeof(half4); + quats->getQuats((quath*) ctx.outBuffer, ctx.quatCount, ctx.outStride); break; case SHORT4: - writeQuat = [] (quatf inquat, uint8_t* outquat) { - *((short4*) outquat) = packSnorm16(inquat.xyzw); - }; - outStride = outStride ? outStride : sizeof(short4); + quats->getQuats((short4*) ctx.outBuffer, ctx.quatCount, ctx.outStride); break; case FLOAT4: - writeQuat = [] (quatf inquat, uint8_t* outquat) { - *((quatf*) outquat) = inquat; - }; - outStride = outStride ? outStride : sizeof(float4); + quats->getQuats((quatf*) ctx.outBuffer, ctx.quatCount, ctx.outStride); break; } - const float3* normal = ctx.normals; - const size_t nstride = ctx.normalsStride ? ctx.normalsStride : sizeof(float3); - uint8_t* outquat = (uint8_t*) ctx.outBuffer; - - // If tangents are not provided, simply cross N with arbitrary vector (1, 0, 0) - if (!ctx.tangents) { - for (size_t qindex = 0, qcount = ctx.quatCount; qindex < qcount; ++qindex) { - float3 n = *normal; - float3 b = normalize(cross(n, float3{1, 0, 0})); - float3 t = cross(n, b); - writeQuat(mat3f::packTangentFrame({t, b, n}), outquat); - normal = (const float3*) (((const uint8_t*) normal) + nstride); - outquat += outStride; - } - return; - } - - const float3* tanvec = &ctx.tangents->xyz; - const float* tandir = &ctx.tangents->w; - const size_t tstride = ctx.tangentsStride ? ctx.tangentsStride : sizeof(float4); - - for (size_t qindex = 0, qcount = ctx.quatCount; qindex < qcount; ++qindex) { - float3 n = *normal; - float3 t = *tanvec; - float3 b = *tandir > 0 ? cross(t, n) : cross(n, t); - writeQuat(mat3f::packTangentFrame({t, b, n}), outquat); - normal = (const float3*) (((const uint8_t*) normal) + nstride); - tanvec = (const float3*) (((const uint8_t*) tanvec) + tstride); - tandir = (const float*) (((const uint8_t*) tandir) + tstride); - outquat += outStride; - } + geometry::SurfaceOrientation::destroy(quats); } } // namespace filament diff --git a/libs/geometry/CMakeLists.txt b/libs/geometry/CMakeLists.txt new file mode 100644 index 000000000000..0012fdb6a3c2 --- /dev/null +++ b/libs/geometry/CMakeLists.txt @@ -0,0 +1,48 @@ +cmake_minimum_required(VERSION 3.1) +project(geometry) + +set(TARGET geometry) +set(PUBLIC_HDR_DIR include) + +# ================================================================================================== +# Sources and headers +# ================================================================================================== +set(PUBLIC_HDRS + include/geometry/SurfaceOrientation.h +) + +set(SRCS + src/SurfaceOrientation.cpp +) + +# ================================================================================================== +# Include and target definitions +# ================================================================================================== +include_directories(${PUBLIC_HDR_DIR}) + +add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS}) + +target_link_libraries(${TARGET} PUBLIC math utils) + +target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR}) + +# ================================================================================================== +# Compiler flags +# ================================================================================================== +target_compile_options(${TARGET} PRIVATE -Wno-deprecated-register) + +if (MSVC OR CLANG_CL) + target_compile_options(${TARGET} PRIVATE $<$:/fp:fast>) +else() + target_compile_options(${TARGET} PRIVATE $<$:-ffast-math>) +endif() + +if (LINUX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") +endif() + +# ================================================================================================== +# Installation +# ================================================================================================== +install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR}) +install(DIRECTORY ${PUBLIC_HDR_DIR}/geometry DESTINATION include) diff --git a/libs/geometry/include/geometry/SurfaceOrientation.h b/libs/geometry/include/geometry/SurfaceOrientation.h new file mode 100644 index 000000000000..cc750d7231db --- /dev/null +++ b/libs/geometry/include/geometry/SurfaceOrientation.h @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef TNT_GEOMETRY_SURFACEORIENTATION_H +#define TNT_GEOMETRY_SURFACEORIENTATION_H + +#include +#include +#include + +namespace filament { +namespace geometry { + +struct OrientationBuilderImpl; +struct OrientationImpl; + +/** + * The surface orientation helper can be used to populate Filament-style TANGENTS buffers. + */ +class SurfaceOrientation { +public: + + /** + * Constructs an immutable surface orientation helper. + * + * At a minimum, clients must supply a vertex count and normals buffer. They can supply data in + * any of the following three combinations: + * + * 1. vec3 normals only (not recommended) + * 2. vec3 normals + vec4 tangents (sign of W determines bitangent orientation) + * 3. vec3 normals + vec2 uvs + vec3 positions + uint3 indices + */ + class Builder { + public: + Builder(); + ~Builder() noexcept; + + /** + * These two attributes are required. They are not passed into the constructor to force + * calling code to be self-documenting. + * @{ + */ + Builder& vertexCount(size_t vertexCount) noexcept; + Builder& normals(const filament::math::float3*) noexcept; + /** @} */ + + Builder& tangents(const filament::math::float4*) noexcept; + Builder& uvs(const filament::math::float2*) noexcept; + Builder& positions(const filament::math::float3*) noexcept; + + Builder& triangleCount(size_t triangleCount) noexcept; + Builder& triangles(const filament::math::uint3*) noexcept; + Builder& triangles(const filament::math::ushort3*) noexcept; + + /** + * Supplying explicit stride values is optional, they each default to the size of their + * respective input attributes, e.g. sizeof(float3) for normals. + * @{ + */ + Builder& normalStride(size_t numBytes) noexcept; + Builder& tangentStride(size_t numBytes) noexcept; + Builder& uvStride(size_t numBytes) noexcept; + Builder& positionStride(size_t numBytes) noexcept; + /** @} */ + + /** + * Generates quats or panics if the submitted data is an incomplete combination. + */ + SurfaceOrientation* build(); + + private: + OrientationBuilderImpl* mImpl; + Builder(const Builder&); + Builder& operator=(const Builder&); + }; + + static void destroy(SurfaceOrientation* so); + + /** + * Returns the vertex count. + */ + size_t getVertexCount() const noexcept; + + /** + * Converts quaternions into the desired output format and writes up to "nquats" + * to the given output pointer. Normally nquats should be equal to the vertex count. + * The optional stride is the desired quat-to-quat stride in bytes. + * @{ + */ + void getQuats(filament::math::quatf* out, size_t nquats, size_t stride = 0) const noexcept; + void getQuats(filament::math::short4* out, size_t nquats, size_t stride = 0) const noexcept; + void getQuats(filament::math::quath* out, size_t nquats, size_t stride = 0) const noexcept; + /** @} */ + +private: + SurfaceOrientation(OrientationImpl*); + ~SurfaceOrientation(); + OrientationImpl* mImpl; + friend struct OrientationBuilderImpl; +}; + +} // namespace geometry +} // namespace filament + +#endif // TNT_GEOMETRY_SURFACEORIENTATION_H diff --git a/libs/geometry/src/SurfaceOrientation.cpp b/libs/geometry/src/SurfaceOrientation.cpp new file mode 100644 index 000000000000..bd7fa34c4894 --- /dev/null +++ b/libs/geometry/src/SurfaceOrientation.cpp @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#include +#include + +#include + +namespace filament { +namespace geometry { + +using namespace filament::math; +using std::vector; +using Builder = SurfaceOrientation::Builder; + +struct OrientationBuilderImpl { + size_t vertexCount = 0; + const float3* normals = 0; + const float4* tangents = 0; + const float2* uvs = 0; + const float3* positions = 0; + const uint3* triangles32 = 0; + const ushort3* triangles16 = 0; + size_t normalStride = 0; + size_t tangentStride = 0; + size_t uvStride = 0; + size_t positionStride = 0; + size_t triangleCount = 0; + SurfaceOrientation* buildWithNormalsOnly(); + SurfaceOrientation* buildWithSuppliedTangents(); + SurfaceOrientation* buildWithUvs(); +}; + +struct OrientationImpl { + vector quaternions; +}; + +Builder::Builder() : mImpl(new OrientationBuilderImpl) {} + +Builder::~Builder() noexcept { delete mImpl; } + +Builder& Builder::vertexCount(size_t vertexCount) noexcept { + mImpl->vertexCount = vertexCount; + return *this; +} + +Builder& Builder::normals(const float3* normals) noexcept { + mImpl->normals = normals; + return *this; +} + +Builder& Builder::tangents(const float4* tangents) noexcept { + mImpl->tangents = tangents; + return *this; +} + +Builder& Builder::uvs(const float2* uvs) noexcept { + mImpl->uvs = uvs; + return *this; +} + +Builder& Builder::positions(const float3* positions) noexcept { + mImpl->positions = positions; + return *this; +} + +Builder& Builder::triangleCount(size_t triangleCount) noexcept { + mImpl->triangleCount = triangleCount; + return *this; +} + +Builder& Builder::triangles(const uint3* triangles) noexcept { + ASSERT_PRECONDITION(mImpl->triangles16 == nullptr, "Triangles already supplied."); + mImpl->triangles32 = triangles; + return *this; +} + +Builder& Builder::triangles(const ushort3* triangles) noexcept { + ASSERT_PRECONDITION(mImpl->triangles32 == nullptr, "Triangles already supplied."); + mImpl->triangles16 = triangles; + return *this; +} + +Builder& Builder::normalStride(size_t numBytes) noexcept { + mImpl->normalStride = numBytes; + return *this; +} + +Builder& Builder::tangentStride(size_t numBytes) noexcept { + mImpl->tangentStride = numBytes; + return *this; +} + +Builder& Builder::uvStride(size_t numBytes) noexcept { + mImpl->uvStride = numBytes; + return *this; +} + +Builder& Builder::positionStride(size_t numBytes) noexcept { + mImpl->positionStride = numBytes; + return *this; +} + +SurfaceOrientation* Builder::build() { + ASSERT_PRECONDITION(mImpl->normals != nullptr, "Normals are required."); + ASSERT_PRECONDITION(mImpl->vertexCount > 0, "Vertex count must be non-zero."); + if (mImpl->tangents != nullptr) { + return mImpl->buildWithSuppliedTangents(); + } + if (mImpl->uvs == nullptr) { + return mImpl->buildWithNormalsOnly(); + } + bool hasTriangles = mImpl->triangles16 || mImpl->triangles32; + ASSERT_PRECONDITION(hasTriangles && mImpl->positions, + "When using UVs, positions and triangles are required."); + ASSERT_PRECONDITION(mImpl->triangleCount > 0, + "When using UVs, triangle count is required."); + return mImpl->buildWithUvs(); +} + +SurfaceOrientation* OrientationBuilderImpl::buildWithNormalsOnly() { + vector quats(vertexCount); + + const float3* normal = this->normals; + size_t nstride = this->normalStride ? this->normalStride : sizeof(float3); + + for (size_t qindex = 0; qindex < vertexCount; ++qindex) { + float3 n = *normal; + float3 b = normalize(cross(n, float3{1, 0, 0})); + float3 t = cross(n, b); + quats[qindex] = mat3f::packTangentFrame({t, b, n}); + normal = (const float3*) (((const uint8_t*) normal) + nstride); + } + + return new SurfaceOrientation(new OrientationImpl( { std::move(quats) } )); +} + +SurfaceOrientation* OrientationBuilderImpl::buildWithSuppliedTangents() { + vector quats(vertexCount); + + const float3* normal = this->normals; + size_t nstride = this->normalStride ? this->normalStride : sizeof(float3); + + const float3* tanvec = (const float3*) this->tangents; + const float* tandir = &this->tangents->w; + size_t tstride = this->tangentStride ? this->tangentStride : sizeof(float4); + + for (size_t qindex = 0; qindex < vertexCount; ++qindex) { + float3 n = *normal; + float3 t = *tanvec; + float3 b = *tandir < 0 ? cross(t, n) : cross(n, t); + quats[qindex] = mat3f::packTangentFrame({t, b, n}); + normal = (const float3*) (((const uint8_t*) normal) + nstride); + tanvec = (const float3*) (((const uint8_t*) tanvec) + tstride); + tandir = (const float*) (((const uint8_t*) tandir) + tstride); + } + + return new SurfaceOrientation(new OrientationImpl( { std::move(quats) } )); +} + +// This method is based on: +// +// Computing Tangent Space Basis Vectors for an Arbitrary Mesh (Lengyel’s Method) +// http://www.terathon.com/code/tangent.html +// +// We considered mikktspace (which thankfully has a zlib-style license) but it would require +// re-indexing via meshoptimizer and is therefore a bit heavyweight. +// +SurfaceOrientation* OrientationBuilderImpl::buildWithUvs() { + ASSERT_PRECONDITION(this->normalStride == 0, "Non-zero normal stride not yet supported."); + ASSERT_PRECONDITION(this->tangentStride == 0, "Non-zero tangent stride not yet supported."); + ASSERT_PRECONDITION(this->uvStride == 0, "Non-zero uv stride not yet supported."); + ASSERT_PRECONDITION(this->positionStride == 0, "Non-zero positions stride not yet supported."); + ASSERT_PRECONDITION(this->triangles32 == nullptr, "32-bit indices not yet supported."); + + vector tan1(vertexCount); + vector tan2(vertexCount); + memset(tan1.data(), 0, sizeof(float3) * vertexCount); + memset(tan2.data(), 0, sizeof(float3) * vertexCount); + const ushort3* triangle = triangles16; + for (size_t a = 0; a < triangleCount; ++a, ++triangle) { + size_t i1 = triangle->x; + size_t i2 = triangle->y; + size_t i3 = triangle->z; + const float3& v1 = positions[i1]; + const float3& v2 = positions[i2]; + const float3& v3 = positions[i3]; + const float2& w1 = uvs[i1]; + const float2& w2 = uvs[i2]; + const float2& w3 = uvs[i3]; + float x1 = v2.x - v1.x; + float x2 = v3.x - v1.x; + float y1 = v2.y - v1.y; + float y2 = v3.y - v1.y; + float z1 = v2.z - v1.z; + float z2 = v3.z - v1.z; + float s1 = w2.x - w1.x; + float s2 = w3.x - w1.x; + float t1 = w2.y - w1.y; + float t2 = w3.y - w1.y; + float r = 1.0F / (s1 * t2 - s2 * t1); + float3 sdir((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, + (t2 * z1 - t1 * z2) * r); + float3 tdir((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, + (s1 * z2 - s2 * z1) * r); + tan1[i1] += sdir; + tan1[i2] += sdir; + tan1[i3] += sdir; + tan2[i1] += tdir; + tan2[i2] += tdir; + tan2[i3] += tdir; + } + + vector quats(vertexCount); + for (size_t a = 0; a < vertexCount; a++) { + const float3& n = normals[a]; + const float3& t1 = tan1[a]; + const float3& t2 = tan2[a]; + + // Gram-Schmidt orthogonalize + float3 t = normalize(t1 - n * dot(n, t1)); + + // Calculate handedness + float w = (dot(cross(n, t1), t2) < 0.0f) ? -1.0f : 1.0f; + + float3 b = w < 0 ? cross(t, n) : cross(n, t); + quats[a] = mat3f::packTangentFrame({t, b, n}); + } + return new SurfaceOrientation(new OrientationImpl( { std::move(quats) } )); +} + +void SurfaceOrientation::destroy(SurfaceOrientation* so) { delete so; } + +SurfaceOrientation::SurfaceOrientation(OrientationImpl* impl) : mImpl(impl) {} + +SurfaceOrientation::~SurfaceOrientation() { delete mImpl; } + +size_t SurfaceOrientation::getVertexCount() const noexcept { + return mImpl->quaternions.size(); +} + +void SurfaceOrientation::getQuats(quatf* out, size_t nquats, size_t stride) const noexcept { + const vector& in = mImpl->quaternions; + nquats = std::min(nquats, in.size()); + stride = stride ? stride : sizeof(decltype(*out)); + for (size_t i = 0; i < nquats; ++i) { + *out = in[i]; + out = (decltype(out)) (((uint8_t*) out) + stride); + } +} + +void SurfaceOrientation::getQuats(short4* out, size_t nquats, size_t stride) const noexcept { + const vector& in = mImpl->quaternions; + nquats = std::min(nquats, in.size()); + stride = stride ? stride : sizeof(decltype(*out)); + for (size_t i = 0; i < nquats; ++i) { + *out = packSnorm16(in[i].xyzw); + out = (decltype(out)) (((uint8_t*) out) + stride); + } +} + +void SurfaceOrientation::getQuats(quath* out, size_t nquats, size_t stride) const noexcept { + const vector& in = mImpl->quaternions; + nquats = std::min(nquats, in.size()); + stride = stride ? stride : sizeof(decltype(*out)); + for (size_t i = 0; i < nquats; ++i) { + *out = quath(in[i]); + out = (decltype(out)) (((uint8_t*) out) + stride); + } +} + +} // namespace geometry +} // namespace filament