From c7848efd86ca6e544765a073cb697c80a1076526 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 28 Feb 2019 15:05:49 -0800 Subject: [PATCH] Introduce the gltfio library. This is our new mobile-friendly and web-friendly library for loading glTF assets. It is still a work in progress, but already capable of loading many conformance models, including those with animation, skinning, and a couple of extensions (nonlit and texture transforms). Next week we will add a sample app demonstrating its usage. --- CMakeLists.txt | 1 + README.md | 3 +- libs/gltfio/CMakeLists.txt | 56 ++ libs/gltfio/include/gltfio/Animator.h | 67 ++ libs/gltfio/include/gltfio/AssetLoader.h | 145 ++++ libs/gltfio/include/gltfio/FilamentAsset.h | 148 ++++ libs/gltfio/include/gltfio/ResourceLoader.h | 71 ++ libs/gltfio/src/Animator.cpp | 360 ++++++++++ libs/gltfio/src/AssetLoader.cpp | 725 ++++++++++++++++++++ libs/gltfio/src/FFilamentAsset.h | 175 +++++ libs/gltfio/src/FilamentAsset.cpp | 74 ++ libs/gltfio/src/GltfEnums.h | 233 +++++++ libs/gltfio/src/MaterialGenerator.cpp | 316 +++++++++ libs/gltfio/src/MaterialGenerator.h | 91 +++ libs/gltfio/src/ResourceLoader.cpp | 412 +++++++++++ libs/gltfio/src/math.h | 110 +++ libs/gltfio/src/upcast.h | 48 ++ 17 files changed, 3034 insertions(+), 1 deletion(-) create mode 100644 libs/gltfio/CMakeLists.txt create mode 100644 libs/gltfio/include/gltfio/Animator.h create mode 100644 libs/gltfio/include/gltfio/AssetLoader.h create mode 100644 libs/gltfio/include/gltfio/FilamentAsset.h create mode 100644 libs/gltfio/include/gltfio/ResourceLoader.h create mode 100644 libs/gltfio/src/Animator.cpp create mode 100644 libs/gltfio/src/AssetLoader.cpp create mode 100644 libs/gltfio/src/FFilamentAsset.h create mode 100644 libs/gltfio/src/FilamentAsset.cpp create mode 100644 libs/gltfio/src/GltfEnums.h create mode 100644 libs/gltfio/src/MaterialGenerator.cpp create mode 100644 libs/gltfio/src/MaterialGenerator.h create mode 100644 libs/gltfio/src/ResourceLoader.cpp create mode 100644 libs/gltfio/src/math.h create mode 100644 libs/gltfio/src/upcast.h diff --git a/CMakeLists.txt b/CMakeLists.txt index aa1f4b08de..d03842b29c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -385,6 +385,7 @@ endif() if (NOT ANDROID AND NOT WEBGL AND NOT IOS) add_subdirectory(${LIBRARIES}/bluegl) add_subdirectory(${LIBRARIES}/filagui) + add_subdirectory(${LIBRARIES}/gltfio) add_subdirectory(${LIBRARIES}/imageio) add_subdirectory(${FILAMENT}/java/filamat) diff --git a/README.md b/README.md index 9bf1ced676..cdb4033157 100644 --- a/README.md +++ b/README.md @@ -125,8 +125,9 @@ and tools. - `filaflat`: Serialization/deserialization library used for materials - `filagui`: Helper library for [Dear ImGui](https://github.com/ocornut/imgui) - `filamat`: Material generation library - - `filameshio`: Tiny mesh parsing library (see also `tools/filamesh`) + - `filameshio`: Tiny filamesh parsing library (see also `tools/filamesh`) - `geometry`: Mesh-related utilities + - `gltfio`: Loader for glTF 2.0 - `image`: Image filtering and simple transforms - `imageio`: Image file reading / writing, only intended for internal use - `math`: Math library diff --git a/libs/gltfio/CMakeLists.txt b/libs/gltfio/CMakeLists.txt new file mode 100644 index 0000000000..90a4913380 --- /dev/null +++ b/libs/gltfio/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.1) +project(gltfio) + +set(TARGET gltfio) +set(PUBLIC_HDR_DIR include) + +# ================================================================================================== +# Sources and headers +# ================================================================================================== +set(PUBLIC_HDRS + include/gltfio/Animator.h + include/gltfio/AssetLoader.h + include/gltfio/ResourceLoader.h + include/gltfio/FilamentAsset.h +) + +set(SRCS + src/Animator.cpp + src/AssetLoader.cpp + src/ResourceLoader.cpp + src/FFilamentAsset.h + src/FilamentAsset.cpp + src/GltfEnums.h + src/MaterialGenerator.cpp + src/MaterialGenerator.h + src/math.h + src/upcast.h +) + +# ================================================================================================== +# Include and target definitions +# ================================================================================================== +include_directories(${PUBLIC_HDR_DIR}) + +add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS}) + +target_link_libraries(${TARGET} PUBLIC math utils filamat filament cgltf stb geometry) + +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() + +# ================================================================================================== +# Installation +# ================================================================================================== +install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR}) +install(DIRECTORY ${PUBLIC_HDR_DIR}/gltfio DESTINATION include) diff --git a/libs/gltfio/include/gltfio/Animator.h b/libs/gltfio/include/gltfio/Animator.h new file mode 100644 index 0000000000..bb56049340 --- /dev/null +++ b/libs/gltfio/include/gltfio/Animator.h @@ -0,0 +1,67 @@ +/* + * 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 GLTFIO_ANIMATOR_H +#define GLTFIO_ANIMATOR_H + +#include + +namespace gltfio { + +namespace details { class FFilamentAsset; } + +struct AnimatorImpl; + +/** + * Animator can be used for two things: (1) updating matrices in Transform components + * according to glTF animation definitions and (2) updating bone matrices in Renderable components + * according to glTF skin definitions. + * + * For a usage example, see the comment block for AssetLoader. + * + * TODO: add support for morphing. + */ +class Animator { +public: + /** + * Uses TransformManager to apply rotation, translation, and scale to entities that have + * been targeted by the given animation definition. + */ + void applyAnimation(size_t animationIndex, float time) const; + + /** + * Uses TransformManager to compute root-to-node transforms for all bone nodes, then passes + * the results into RenderableManager::setBones. + * + * Note that this operation is actually independent of animation, but the Animator seems + * like a reasonable place for a utility like this. + */ + void updateBoneMatrices(); + + size_t getAnimationCount() const; + float getAnimationDuration(size_t animationIndex) const; + const char* getAnimationName(size_t animationIndex) const; + +private: + friend class details::FFilamentAsset; + Animator(FilamentAsset* asset); + ~Animator(); + AnimatorImpl* mImpl; +}; + +} // namespace gltfio + +#endif // GLTFIO_ANIMATOR_H diff --git a/libs/gltfio/include/gltfio/AssetLoader.h b/libs/gltfio/include/gltfio/AssetLoader.h new file mode 100644 index 0000000000..666a0a884d --- /dev/null +++ b/libs/gltfio/include/gltfio/AssetLoader.h @@ -0,0 +1,145 @@ +/* + * 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 GLTFIO_ASSETLOADER_H +#define GLTFIO_ASSETLOADER_H + +#include +#include + +#include + +#include + +namespace gltfio { + +/** + * AssetLoader consumes a blob of glTF 2.0 content (either JSON or GLB) and produces an "asset", + * which is a bundle of Filament entities, material instances, textures, vertex buffers, and index + * buffers. + * + * AssetLoader does not fetch external buffer data or create textures on its own. Clients can use + * the provided ResourceLoader class for this, which obtains the URI list from the asset. This is + * demonstrated in the code snippet below. + * + * AssetLoader also owns a cache of Material objects that may be re-used across multiple loads. + * + * TODO: currently this uses filamat to generate materials on the fly, but we may wish to allow + * clients to load a small set of precompiled ubershader materials to avoid the footprint of the + * filamat library on resource-constrained platforms. + * + * Example usage: + * + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + * auto engine = Engine::create(); + * auto loader = AssetLoader::create(engine); + * + * // Parse the glTF content and create Filament entities. + * std::vector content(...); + * FilamentAsset* asset = loader->createAssetFromJson(content.data(), content.size()); + * content.clear(); + * + * // Load buffers and textures from disk. + * ResourceLoader({engine, ".", true}).loadResources(asset); + * + * // Obtain the simple animation interface. + * Animator* animator = asset->getAnimator(); + * + * // Free the glTF hierarchy as it is no longer needed. + * asset->releaseSourceData(); + * + * // Add renderables to the scene. + * scene->addEntities(asset->getEntities(), asset->getEntityCount()); + * + * // Execute the render loop and play the first animation. + * do { + * animator->applyAnimation(0, time); + * animator->updateBoneMatrices(); + * if (renderer->beginFrame(swapChain)) { + * renderer->render(view); + * renderer->endFrame(); + * } + * } while (!quit); + * + * loader->destroyAsset(asset); + * loader->destroyMaterials(); + * AssetLoader::destroy(&loader); + * Engine::destroy(&engine); + * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + */ +class AssetLoader { +public: + + /** + * Creates an asset loader for the given engine. + * + * The engine is held weakly, used only for the creation and destruction of Filament objects. + * The optional name component manager can be used to assign names to renderables. + */ + static AssetLoader* create(filament::Engine* engine, utils::NameComponentManager* = nullptr); + + /** + * Frees the loader. + * + * This does not not automatically free the cache of materials (see destroyMaterials), nor + * does it free the entities for created assets (see destroyAsset). + */ + static void destroy(AssetLoader** loader); + + /** + * Takes a pointer to the contents of a JSON-based glTF 2.0 file and returns a bundle + * of Filament objects. Returns null on failure. + */ + FilamentAsset* createAssetFromJson(uint8_t const* bytes, uint32_t nbytes); + + /** + * Takes a pointer to the contents of a GLB glTF 2.0 file and returns a bundle + * of Filament objects. Returns null on failure. + */ + FilamentAsset* createAssetFromBinary(uint8_t const* bytes, uint32_t nbytes); + + /** Destroys the given asset and all of its associated Filament objects. */ + void destroyAsset(const FilamentAsset* asset); + + /** + * Enables or disables shadows on all subsequently loaded assets. + * + * Initially, loaded assets will cast and receive shadows. + */ + void castShadowsByDefault(bool enable); + void receiveShadowsByDefault(bool enable); + + /** Gets cached materials, used internally to create material instances for assets. */ + size_t getMaterialsCount() const noexcept; + const filament::Material* const* getMaterials() const noexcept; + + /** Destroys all cached materials. */ + void destroyMaterials(); + +protected: + AssetLoader() noexcept = default; + ~AssetLoader() = default; + +public: + AssetLoader(AssetLoader const&) = delete; + AssetLoader(AssetLoader&&) = delete; + AssetLoader& operator=(AssetLoader const&) = delete; + AssetLoader& operator=(AssetLoader&&) = delete; +}; + +} // namespace gltfio + +#endif // GLTFIO_ASSETLOADER_H diff --git a/libs/gltfio/include/gltfio/FilamentAsset.h b/libs/gltfio/include/gltfio/FilamentAsset.h new file mode 100644 index 0000000000..ee32a5624d --- /dev/null +++ b/libs/gltfio/include/gltfio/FilamentAsset.h @@ -0,0 +1,148 @@ +/* + * 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 GLTFIO_FILAMENTASSET_H +#define GLTFIO_FILAMENTASSET_H + +#include +#include + +#include + +namespace filament { + class Engine; + class IndexBuffer; + class MaterialInstance; + class VertexBuffer; +} + +namespace gltfio { + +struct BufferBinding; +struct TextureBinding; +class Animator; + +/** + * FilamentAsset owns a bundle of Filament objects that have been created by AssetLoader. + * For usage instructions, see the comment block for AssetLoader. + * + * This class owns a hierarchy of entities that have been loaded from a glTF asset. Every entity has + * a TransformManager component, and some entities also have Name and/or Renderable components. + * + * In addition to the aforementioned entities, an asset has strong ownership over a list of + * VertexBuffer, IndexBuffer, MaterialInstance, Texture, and, optionally, a simple animation engine. + * + * Clients must use ResourceLoader to create Texture objects, compute tangent quaternions, and + * upload data into vertex buffers and index buffers. + * + * TODO: This supports skinning but not morphing. + * TODO: Only the default glTF scene is loaded, other glTF scenes are ignored. + * TODO: Cameras, extras, and extensions are ignored. + */ +class FilamentAsset { +public: + + /** + * Gets the list of entities, one for each glTF node. All of these have a Transform component. + * Some of the returned entities may also have a Renderable component. + */ + size_t getEntityCount() const noexcept; + const utils::Entity* getEntities() const noexcept; + + /** Gets the transform root for the asset, which has no matching glTF node. */ + utils::Entity getRoot() const noexcept; + + /** Gets all material instances. These are already bound to renderables. */ + size_t getMaterialInstanceCount() const noexcept; + const filament::MaterialInstance* const* getMaterialInstances() const noexcept; + + /** Gets loading instructions for vertex buffers and index buffers. */ + size_t getBufferBindingCount() const noexcept; + const BufferBinding* getBufferBindings() const noexcept; + + /** Gets loading instructions for textures. */ + size_t getTextureBindingCount() const noexcept; + const TextureBinding* getTextureBindings() const noexcept; + + /** Gets the bounding box computed from the supplied min / max values in glTF accessors. */ + filament::Aabb getBoundingBox() const noexcept; + + /** + * Lazily creates the animation engine or returns it from the cache. + * The animator is owned by the asset and should not be manually deleted. + */ + Animator* getAnimator() noexcept; + + /** + * Reclaims CPU-side memory for URI strings, binding lists, and raw animation data. + * + * This should only be called after ResourceLoader::loadResources(). + * If using Animator, this should be called after getAnimator(). + */ + void releaseSourceData() noexcept; + +protected: + FilamentAsset() noexcept = default; + ~FilamentAsset() = default; + +public: + FilamentAsset(FilamentAsset const&) = delete; + FilamentAsset(FilamentAsset&&) = delete; + FilamentAsset& operator=(FilamentAsset const&) = delete; + FilamentAsset& operator=(FilamentAsset&&) = delete; +}; + +/** + * BufferBinding is a read-only structure that tells clients how to load a source blob into a + * VertexBuffer slot or IndexBuffer. + * + * Each binding instance corresponds to one of the following: + * + * (a) One call to VertexBuffer::setBufferAt(). + * (b) One call to IndexBuffer::setBuffer(). + * + */ +struct BufferBinding { + const char* uri; // unique identifier for the source blob + uint32_t totalSize; // size in bytes of the source blob at the given URI + int bufferIndex; // only used when the destination is a VertexBuffer + uint32_t offset; // byte count used only for vertex and index buffers + uint32_t size; // byte count used only for vertex and index buffers + void** data; // pointer to the resource data in the source asset (if loaded) + + // Only one of the following two destinations can be non-null. + filament::VertexBuffer* vertexBuffer; + filament::IndexBuffer* indexBuffer; + + bool convertBytesToShorts; // the resource loader must convert the buffer from u8 to u16 + bool generateTrivialIndices; // the resource loader must generate indices like: 0, 1, 2, ... +}; + +/** Describes a binding from a Texture to a MaterialInstance. */ +struct TextureBinding { + const char* uri; + uint32_t totalSize; + const char* mimeType; + void** data; + filament::MaterialInstance* materialInstance; + const char* materialParameter; + filament::TextureSampler sampler; + bool srgb; +}; + +} // namespace gltfio + +#endif // GLTFIO_FILAMENTASSET_H diff --git a/libs/gltfio/include/gltfio/ResourceLoader.h b/libs/gltfio/include/gltfio/ResourceLoader.h new file mode 100644 index 0000000000..996eec10bc --- /dev/null +++ b/libs/gltfio/include/gltfio/ResourceLoader.h @@ -0,0 +1,71 @@ +/* + * 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 GLTFIO_RESOURCELOADER_H +#define GLTFIO_RESOURCELOADER_H + +#include + +#include + +#include + +namespace gltfio { + +namespace details { + class FFilamentAsset; + class AssetPool; +} + +struct ResourceConfiguration { + class filament::Engine* engine; + utils::Path basePath; + bool normalizeSkinningWeights; +}; + +/** + * ResourceLoader asynchronously uploads vertex buffers and textures to the GPU, computes + * surface orientation quaternions, and optionally normalizes skinning weights. + * + * For a usage example, see the comment block for AssetLoader. + * + * In theory, this class could cache a map of URL's to data blobs and could therefore be useful + * across multiple assets. However, clients should feel free to immediately destroy this after + * calling loadResources. There is no need to wait for resources to finish uploading because this is + * done in the the background. + * + * The resource loader must be destroyed on the same thread that calls Renderer::render because it + * listens to BufferDescriptor callbacks in order to determine when to free CPU-side data blobs. + * + * TODO: the GPU upload is asynchronous but the load-from-disk and image decode is not. + */ +class ResourceLoader { +public: + ResourceLoader(const ResourceConfiguration& config); + ~ResourceLoader(); + bool loadResources(FilamentAsset* asset); +private: + bool createTextures(details::FFilamentAsset* asset) const; + void computeTangents(details::FFilamentAsset* asset) const; + void normalizeWeights(details::FFilamentAsset* asset) const; + details::AssetPool* mPool; + const ResourceConfiguration mConfig; +}; + +} // namespace gltfio + +#endif // GLTFIO_RESOURCELOADER_H + diff --git a/libs/gltfio/src/Animator.cpp b/libs/gltfio/src/Animator.cpp new file mode 100644 index 0000000000..15ba419812 --- /dev/null +++ b/libs/gltfio/src/Animator.cpp @@ -0,0 +1,360 @@ +/* + * 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 "FFilamentAsset.h" +#include "math.h" +#include "upcast.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace filament; +using namespace filament::math; +using namespace std; +using namespace utils; + +namespace gltfio { + +using namespace details; + +using TimeValues = std::map; +using SourceValues = std::vector; + +struct Sampler { + TimeValues times; + SourceValues values; + enum { LINEAR, STEP, CUBIC } interpolation; +}; + +struct Channel { + const Sampler* sourceData; + utils::Entity targetEntity; + enum { TRANSLATION, ROTATION, SCALE } transformType; +}; + +struct Animation { + float duration; + std::string name; + std::vector samplers; + std::vector channels; +}; + +struct AnimatorImpl { + std::vector animations; + FFilamentAsset* asset; + RenderableManager* renderableManager; + TransformManager* transformManager; +}; + +static int numComponents(cgltf_type type) { + switch (type) { + case cgltf_type_vec3: return 3; + case cgltf_type_vec4: return 4; + default: return 1; + } +} + +static void convert8(const cgltf_accessor* src, const uint8_t* srcBlob, SourceValues& dst) { + srcBlob += src->buffer_view->offset + src->offset; + dst.resize(src->count * numComponents(src->type)); + auto srcValues = (const int8_t*) srcBlob; + for (size_t i = 0, n = dst.size(); i < n; ++i) { + dst[i] = unpackSnorm8(srcValues[i]); + } +} + +static void convert8U(const cgltf_accessor* src, const uint8_t* srcBlob, SourceValues& dst) { + srcBlob += src->buffer_view->offset + src->offset; + dst.resize(src->count * numComponents(src->type)); + for (size_t i = 0, n = dst.size(); i < n; ++i) { + dst[i] = unpackUnorm8(srcBlob[i]); + } +} + +static void convert16(const cgltf_accessor* src, const uint8_t* srcBlob, SourceValues& dst) { + srcBlob += src->buffer_view->offset + src->offset; + dst.resize(src->count * numComponents(src->type)); + auto srcValues = (const int16_t*) srcBlob; + for (size_t i = 0, n = dst.size(); i < n; ++i) { + dst[i] = unpackSnorm16(srcValues[i]); + } +} + +static void convert16U(const cgltf_accessor* src, const uint8_t* srcBlob, SourceValues& dst) { + srcBlob += src->buffer_view->offset + src->offset; + dst.resize(src->count * numComponents(src->type)); + auto srcValues = (const uint16_t*) srcBlob; + for (size_t i = 0, n = dst.size(); i < n; ++i) { + dst[i] = unpackUnorm16(srcValues[i]); + } +} + +static void convert32F(const cgltf_accessor* src, const uint8_t* srcBlob, SourceValues& dst) { + srcBlob += src->buffer_view->offset + src->offset; + dst.resize(src->count * numComponents(src->type)); + memcpy(dst.data(), srcBlob, dst.size() * sizeof(float)); +} + +static void createSampler(const cgltf_animation_sampler& src, Sampler& dst) { + // Copy the time values into a red-black tree. + const cgltf_accessor* timelineAccessor = src.input; + const uint8_t* timelineBlob = (const uint8_t*) timelineAccessor->buffer_view->buffer->data; + const float* timelineFloats = (const float*) (timelineBlob + timelineAccessor->offset + + timelineAccessor->buffer_view->offset); + for (size_t i = 0, len = timelineAccessor->count; i < len; ++i) { + dst.times[timelineFloats[i]] = i; + } + + // Convert source data to float. + const cgltf_accessor* valuesAccessor = src.output; + const uint8_t* valuesBlob = (const uint8_t*) valuesAccessor->buffer_view->buffer->data; + switch (valuesAccessor->component_type) { + case cgltf_component_type_r_8: + convert8(valuesAccessor, valuesBlob, dst.values); + break; + case cgltf_component_type_r_8u: + convert8U(valuesAccessor, valuesBlob, dst.values); + break; + case cgltf_component_type_r_16: + convert16(valuesAccessor, valuesBlob, dst.values); + break; + case cgltf_component_type_r_16u: + convert16U(valuesAccessor, valuesBlob, dst.values); + break; + case cgltf_component_type_r_32f: + convert32F(valuesAccessor, valuesBlob, dst.values); + break; + default: + slog.e << "Unknown animation type." << io::endl; + return; + } + + switch (src.interpolation) { + case cgltf_interpolation_type_linear: + dst.interpolation = Sampler::LINEAR; + break; + case cgltf_interpolation_type_step: + dst.interpolation = Sampler::STEP; + break; + case cgltf_interpolation_type_cubic_spline: + dst.interpolation = Sampler::CUBIC; + break; + } +} + +static void setTransformType(const cgltf_animation_channel& src, Channel& dst) { + switch (src.target_path) { + case cgltf_animation_path_type_translation: + dst.transformType = Channel::TRANSLATION; + break; + case cgltf_animation_path_type_rotation: + dst.transformType = Channel::ROTATION; + break; + case cgltf_animation_path_type_scale: + dst.transformType = Channel::SCALE; + break; + case cgltf_animation_path_type_invalid: + case cgltf_animation_path_type_weights: + slog.e << "Unsupported channel path." << io::endl; + break; + } +} + +Animator::Animator(FilamentAsset* publicAsset) { + mImpl = new AnimatorImpl(); + FFilamentAsset* asset = mImpl->asset = upcast(publicAsset); + mImpl->renderableManager = &asset->mEngine->getRenderableManager(); + mImpl->transformManager = &asset->mEngine->getTransformManager(); + + // Loop over the glTF animation definitions. + const cgltf_data* srcAsset = asset->mSourceAsset; + const cgltf_animation* srcAnims = srcAsset->animations; + mImpl->animations.resize(srcAsset->animations_count); + for (cgltf_size i = 0, len = srcAsset->animations_count; i < len; ++i) { + const cgltf_animation& srcAnim = srcAnims[i]; + Animation& dstAnim = mImpl->animations[i]; + dstAnim.duration = 0; + if (srcAnim.name) { + dstAnim.name = srcAnim.name; + } + + // Import each glTF sampler into a custom data structure. + cgltf_animation_sampler* srcSamplers = srcAnim.samplers; + dstAnim.samplers.resize(srcAnim.samplers_count); + for (cgltf_size j = 0, nsamps = srcAnim.samplers_count; j < nsamps; ++j) { + const cgltf_animation_sampler& srcSampler = srcSamplers[j]; + Sampler& dstSampler = dstAnim.samplers[j]; + createSampler(srcSampler, dstSampler); + if (dstSampler.times.size() > 1) { + float maxtime = (--dstSampler.times.end())->first; + dstAnim.duration = std::max(dstAnim.duration, maxtime); + } + } + + // Import each glTF channel into a custom data structure. + cgltf_animation_channel* srcChannels = srcAnim.channels; + dstAnim.channels.resize(srcAnim.channels_count); + auto& transformManager = *mImpl->transformManager; + for (cgltf_size j = 0, nchans = srcAnim.channels_count; j < nchans; ++j) { + const cgltf_animation_channel& srcChannel = srcChannels[j]; + utils::Entity targetEntity = asset->mNodeMap[srcChannel.target_node]; + Channel& dstChannel = dstAnim.channels[j]; + dstChannel.sourceData = &dstAnim.samplers[srcChannel.sampler - srcSamplers]; + dstChannel.targetEntity = targetEntity; + setTransformType(srcChannel, dstChannel); + } + } +} + +Animator::~Animator() { + delete mImpl; +} + +size_t Animator::getAnimationCount() const { + return mImpl->animations.size(); +} + +void Animator::applyAnimation(size_t animationIndex, float time) const { + const Animation& anim = mImpl->animations[animationIndex]; + time = fmod(time, anim.duration); + for (const auto& channel : anim.channels) { + const Sampler* sampler = channel.sourceData; + if (sampler->times.size() < 2) { + continue; + } + + int cindex = &channel - &anim.channels.front(); + int sindex = sampler - &anim.samplers.front(); + + TransformManager::Instance node = mImpl->transformManager->getInstance(channel.targetEntity); + const TimeValues& times = sampler->times; + + // Find the first keyframe after the given time, or the keyframe that matches it exactly. + TimeValues::const_iterator iter = times.lower_bound(time); + + // Find the two values that we will interpolate between. + TimeValues::const_iterator prevIter; + TimeValues::const_iterator nextIter; + if (iter == times.end()) { + prevIter = --times.end(); + nextIter = times.begin(); + } else if (iter == times.begin()) { + prevIter = nextIter = iter; + } else { + nextIter = iter; + prevIter = --iter; + } + + // Compute the interpolant between 0 and 1. + float prevTime = prevIter->first; + float nextTime = nextIter->first; + float interval = nextTime - prevTime; + if (interval < 0) { + interval += anim.duration; + } + float t = interval == 0 ? 0.0f : ((time - prevTime) / interval); + + // Perform the interpolation. This is a simple but inefficient implementation; Filament + // stores transforms as mat4's but glTF animation is based on TRS (translation rotation + // scale). TODO: honor channel.transformType and support STEP / CUBIC interpolation + size_t prevIndex = prevIter->second; + size_t nextIndex = nextIter->second; + + mat4f xform = mImpl->transformManager->getTransform(node); + float3 scale; + quatf rotation; + float3 translation; + decomposeMatrix(xform, &translation, &rotation, &scale); + + switch (channel.transformType) { + case Channel::SCALE: { + const float3* srcVec3 = (const float3*) sampler->values.data(); + scale = ((1 - t) * srcVec3[prevIndex]) + (t * srcVec3[nextIndex]); + break; + } + case Channel::TRANSLATION: { + const float3* srcVec3 = (const float3*) sampler->values.data(); + translation = ((1 - t) * srcVec3[prevIndex]) + (t * srcVec3[nextIndex]); + break; + } + case Channel::ROTATION: { + const quatf* srcQuat = (const quatf*) sampler->values.data(); + rotation = slerp(srcQuat[prevIndex], srcQuat[nextIndex], t); + break; + } + } + + xform = composeMatrix(translation, rotation, scale); + mImpl->transformManager->setTransform(node, xform); + } +} + +void Animator::updateBoneMatrices() { + vector boneMatrices; + FFilamentAsset* asset = mImpl->asset; + auto renderableManager = mImpl->renderableManager; + auto transformManager = mImpl->transformManager; + for (const auto& skin : asset->mSkins) { + size_t njoints = skin.joints.size(); + boneMatrices.resize(njoints); + for (const auto& entity : skin.targets) { + auto renderable = renderableManager->getInstance(entity); + if (!renderable) { + continue; + } + mat4f inverseGlobalTransform; + auto xformable = transformManager->getInstance(entity); + if (xformable) { + inverseGlobalTransform = inverse(transformManager->getWorldTransform(xformable)); + } + for (size_t boneIndex = 0; boneIndex < njoints; ++boneIndex) { + const auto& joint = skin.joints[boneIndex]; + TransformManager::Instance jointInstance = transformManager->getInstance(joint); + mat4f globalJointTransform = transformManager->getWorldTransform(jointInstance); + boneMatrices[boneIndex] = + inverseGlobalTransform * + globalJointTransform * + skin.inverseBindMatrices[boneIndex]; + } + renderableManager->setBones(renderable, boneMatrices.data(), boneMatrices.size()); + } + } +} + +float Animator::getAnimationDuration(size_t animationIndex) const { + return mImpl->animations[animationIndex].duration; +} + +const char* Animator::getAnimationName(size_t animationIndex) const { + return mImpl->animations[animationIndex].name.c_str(); +} + +} // namespace gltfio diff --git a/libs/gltfio/src/AssetLoader.cpp b/libs/gltfio/src/AssetLoader.cpp new file mode 100644 index 0000000000..1ff3a0ad7e --- /dev/null +++ b/libs/gltfio/src/AssetLoader.cpp @@ -0,0 +1,725 @@ +/* + * 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 "FFilamentAsset.h" +#include "GltfEnums.h" +#include "MaterialGenerator.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#define CGLTF_IMPLEMENTATION +#include + +#include "math.h" +#include "upcast.h" + +using namespace filament; +using namespace filament::math; +using namespace utils; + +namespace gltfio { +namespace details { + +// MeshCache +// --------- +// If a given glTF mesh is referenced by multiple glTF nodes, then it generates a separate Filament +// renderable for each of those nodes. All renderables generated by a given mesh share a common set +// of VertexBuffer and IndexBuffer objects. To achieve the sharing behavior, the loader maintains a +// small cache. The cache keys are glTF mesh definitions and the cache entries are lists of +// primitives, where a "primitive" is a reference to a Filament VertexBuffer and IndexBuffer. +struct Primitive { + VertexBuffer* vertices = nullptr; + IndexBuffer* indices = nullptr; + Aabb aabb; // object-space bounding box +}; +using MeshCache = tsl::robin_map>; + +// Filament materials are cached by the MaterialGenerator, but material instances are cached here. +using MatInstanceCache = tsl::robin_map; + +// Filament automatically infers the size of driver-level vertex buffers from the attribute data +// (stride, count, offset) and clients are expected to avoid uploading data blobs that exceed this +// size. Since this information doesn't exist in the glTF we need to compute it manually. This is a +// bit of a cheat, cgltf_calc_size is private but its implementation file is available in this cpp +// file. +static uint32_t computeBindingSize(const cgltf_accessor* accessor){ + cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type); + return uint32_t(accessor->stride * (accessor->count - 1) + element_size); +}; + +static uint32_t computeBindingOffset(const cgltf_accessor* accessor) { + return uint32_t(accessor->offset + accessor->buffer_view->offset); +}; + +struct FAssetLoader : public AssetLoader { + FAssetLoader(Engine* engine, NameComponentManager* names) : + mEntityManager(EntityManager::get()), + mRenderableManager(engine->getRenderableManager()), + mNameManager(names), + mTransformManager(engine->getTransformManager()), + mMaterials(engine), + mEngine(engine) {} + + FFilamentAsset* createAssetFromJson(uint8_t const* bytes, uint32_t nbytes); + FilamentAsset* createAssetFromBinary(uint8_t const* bytes, uint32_t nbytes); + + void destroyAsset(const FFilamentAsset* asset) { + delete asset; + } + + void castShadowsByDefault(bool enable) { + mCastShadows = enable; + } + + void receiveShadowsByDefault(bool enable) { + mReceiveShadows = enable; + } + + size_t getMaterialsCount() const noexcept { + return mMaterials.getMaterialsCount(); + } + + const Material* const* getMaterials() const noexcept { + return mMaterials.getMaterials(); + } + + void destroyMaterials() { + mMaterials.destroyMaterials(); + } + + void createAsset(const cgltf_data* srcAsset); + void createEntity(const cgltf_node* node, Entity parent); + void createRenderable(const cgltf_node* node, Entity entity); + bool createPrimitive(const cgltf_primitive* inPrim, Primitive* outPrim, const UvMap& uvmap); + MaterialInstance* createMaterialInstance(const cgltf_material* inputMat, UvMap* uvmap, + bool vertexColor); + void addTextureBinding(MaterialInstance* materialInstance, const char* parameterName, + const cgltf_texture* srcTexture, bool srgb); + void importSkinningData(Skin& dstSkin, const cgltf_skin& srcSkin); + bool primitiveHasVertexColor(const cgltf_primitive* inPrim) const; + + bool mCastShadows = true; + bool mReceiveShadows = true; + + EntityManager& mEntityManager; + RenderableManager& mRenderableManager; + NameComponentManager* mNameManager; + TransformManager& mTransformManager; + MaterialGenerator mMaterials; + Engine* mEngine; + + // The loader owns a few transient mappings used only for the current asset being loaded. + FFilamentAsset* mResult; + MatInstanceCache mMatInstanceCache; + MeshCache mMeshCache; + bool mError = false; +}; + +FILAMENT_UPCAST(AssetLoader) + +} // namespace details + +using namespace details; + +FFilamentAsset* FAssetLoader::createAssetFromJson(uint8_t const* bytes, uint32_t nbytes) { + cgltf_options options { cgltf_file_type_invalid }; + cgltf_data* sourceAsset; + cgltf_result result = cgltf_parse(&options, bytes, nbytes, &sourceAsset); + if (result != cgltf_result_success) { + return nullptr; + } + createAsset(sourceAsset); + return mResult; +} + +FilamentAsset* FAssetLoader::createAssetFromBinary(uint8_t const* bytes, uint32_t nbytes) { + cgltf_options options { cgltf_file_type_glb }; + cgltf_data* sourceAsset; + cgltf_result result = cgltf_parse(&options, bytes, nbytes, &sourceAsset); + if (result != cgltf_result_success) { + return nullptr; + } + createAsset(sourceAsset); + return mResult; +} + +void FAssetLoader::createAsset(const cgltf_data* srcAsset) { + mResult = new FFilamentAsset(mEngine); + mResult->mSourceAsset = srcAsset; + mResult->acquireSourceAsset(); + + // If there is no default scene specified, then the default is the first one. + // It is not an error for a glTF file to have zero scenes. + const cgltf_scene* scene = srcAsset->scene ? srcAsset->scene : srcAsset->scenes; + if (!scene) { + return; + } + + // Create a single root node with an identity transform as a convenience to the client. + mResult->mRoot = mEntityManager.create(); + mTransformManager.create(mResult->mRoot); + + // One scene may have multiple root nodes. Recurse down and create an entity for each node. + cgltf_node** nodes = scene->nodes; + for (cgltf_size i = 0, len = scene->nodes_count; i < len; ++i) { + const cgltf_node* root = nodes[i]; + createEntity(root, mResult->mRoot); + } + + if (mError) { + delete mResult; + mResult = nullptr; + } + + // Copy over joint lists (references to TransformManager components) and create buffer bindings + // for inverseBindMatrices. + mResult->mSkins.resize(srcAsset->skins_count); + for (cgltf_size i = 0, len = srcAsset->skins_count; i < len; ++i) { + importSkinningData(mResult->mSkins[i], srcAsset->skins[i]); + } + + // For each skin, build a list of renderables that it affects. + for (cgltf_size i = 0, len = srcAsset->nodes_count; i < len; ++i) { + const cgltf_node& node = srcAsset->nodes[i]; + if (node.skin) { + int skinIndex = node.skin - &srcAsset->skins[0]; + Entity entity = mResult->mNodeMap[&node]; + mResult->mSkins[skinIndex].targets.push_back(entity); + } + } + + // We're done with the import, so free up transient bookkeeping resources. + mMatInstanceCache.clear(); + mMeshCache.clear(); + mError = false; +} + +void FAssetLoader::createEntity(const cgltf_node* node, Entity parent) { + Entity entity = mEntityManager.create(); + + // Always create a transform component to reflect the original hierarchy. + mat4f localTransform; + if (node->has_matrix) { + memcpy(&localTransform[0][0], &node->matrix[0], 16 * sizeof(float)); + } else { + quatf* rotation = (quatf*) &node->rotation[0]; + float3* scale = (float3*) &node->scale[0]; + float3* translation = (float3*) &node->translation[0]; + localTransform = composeMatrix(*translation, *rotation, *scale); + } + + auto parentTransform = mTransformManager.getInstance(parent); + mTransformManager.create(entity, parentTransform, localTransform); + + // Update the asset's entity list and private node mapping. + mResult->mEntities.push_back(entity); + mResult->mNodeMap[node] = entity; + + // If the node has a mesh, then create a renderable component. + if (node->mesh) { + createRenderable(node, entity); + } + + for (cgltf_size i = 0, len = node->children_count; i < len; ++i) { + createEntity(node->children[i], entity); + } +} + +void FAssetLoader::createRenderable(const cgltf_node* node, Entity entity) { + const cgltf_mesh* mesh = node->mesh; + + // Compute the transform relative to the root. + auto thisTransform = mTransformManager.getInstance(entity); + mat4f worldTransform = mTransformManager.getWorldTransform(thisTransform); + + cgltf_size nprims = mesh->primitives_count; + RenderableManager::Builder builder(nprims); + + // If the mesh is already loaded, obtain the list of Filament VertexBuffer / IndexBuffer + // objects that were already generated, otherwise allocate a new list of null pointers. + auto iter = mMeshCache.find(mesh); + if (iter == mMeshCache.end()) { + mMeshCache[mesh].resize(nprims); + } + Primitive* outputPrim = mMeshCache[mesh].data(); + const cgltf_primitive* inputPrim = &mesh->primitives[0]; + + if (mNameManager && mesh->name) { + mNameManager->addComponent(entity); + mNameManager->setName(mNameManager->getInstance(entity), mesh->name); + } + + Aabb aabb; + + // For each prim, create a Filament VertexBuffer, IndexBuffer, and MaterialInstance. + for (cgltf_size index = 0; index < nprims; ++index, ++outputPrim, ++inputPrim) { + RenderableManager::PrimitiveType primType; + if (!getPrimitiveType(inputPrim->type, &primType)) { + slog.e << "Unsupported primitive type." << io::endl; + } + + // Create a material instance for this primitive or fetch one from the cache. + UvMap uvmap; + bool hasVertexColor = primitiveHasVertexColor(inputPrim); + MaterialInstance* mi = createMaterialInstance(inputPrim->material, &uvmap, hasVertexColor); + builder.material(index, mi); + + // Create a Filament VertexBuffer and IndexBuffer for this prim if we haven't already. + if (!outputPrim->vertices && !createPrimitive(inputPrim, outputPrim, uvmap)) { + mError = true; + continue; + } + + // Expand the object-space bounding box. + aabb.min = min(outputPrim->aabb.min, aabb.min); + aabb.max = max(outputPrim->aabb.max, aabb.max); + + // We are not using the optional offset, minIndex, maxIndex, and count arguments when + // calling geometry() on the builder. It appears that the glTF spec does not have + // facilities for these parameters, which is not a huge loss since some of the buffer + // view and accessor features already have this functionality. + builder.geometry(index, primType, outputPrim->vertices, outputPrim->indices); + } + + // Expand the world-space bounding box. + float3 minpt = (worldTransform * float4(aabb.min, 1.0)).xyz; + float3 maxpt = (worldTransform * float4(aabb.max, 1.0)).xyz; + + mResult->mBoundingBox.min = min(mResult->mBoundingBox.min, minpt); + mResult->mBoundingBox.max = max(mResult->mBoundingBox.max, maxpt); + + if (node->skin) { + builder.skinning(node->skin->joints_count); + } + + builder + .boundingBox({aabb.min, aabb.max}) + .culling(true) + .castShadows(mCastShadows) + .receiveShadows(mReceiveShadows) + .build(*mEngine, entity); + + // TODO: support vertex morphing by honoring mesh->weights and mesh->weight_count. +} + +bool FAssetLoader::createPrimitive(const cgltf_primitive* inPrim, Primitive* outPrim, + const UvMap& uvmap) { + + // In glTF, each primitive may or may not have an index buffer. If a primitive does not have an + // index buffer, we ask the ResourceLoader to generate a trivial index buffer. + IndexBuffer* indices; + const cgltf_accessor* indicesAccessor = inPrim->indices; + if (indicesAccessor) { + IndexBuffer::Builder ibb; + ibb.indexCount(indicesAccessor->count); + IndexBuffer::IndexType indexType; + if (!getIndexType(indicesAccessor->component_type, &indexType)) { + utils::slog.e << "Unrecognized index type." << utils::io::endl; + return false; + } + ibb.bufferType(indexType); + indices = ibb.build(*mEngine); + const cgltf_buffer_view* bv = indicesAccessor->buffer_view; + mResult->mBufferBindings.emplace_back(BufferBinding { + .uri = bv->buffer->uri, + .totalSize = uint32_t(bv->buffer->size), + .offset = computeBindingOffset(indicesAccessor), + .size = computeBindingSize(indicesAccessor), + .data = &bv->buffer->data, + .indexBuffer = indices, + .convertBytesToShorts = indicesAccessor->component_type == cgltf_component_type_r_8u, + .generateTrivialIndices = false + }); + } else { + const cgltf_size vertexCount = inPrim->attributes[0].data->count; + indices = IndexBuffer::Builder() + .indexCount(vertexCount) + .bufferType(IndexBuffer::IndexType::UINT) + .build(*mEngine); + mResult->mBufferBindings.emplace_back(BufferBinding { + .indexBuffer = indices, + .size = uint32_t(vertexCount * sizeof(uint32_t)), + .generateTrivialIndices = true + }); + } + mResult->mIndexBuffers.push_back(indices); + + // We do not necessarily upload all glTF attribute buffers to the GPU. For example, we do not + // upload tangent vectors in their source format or more than two UV sets. However the buffer + // count that gets passed to the Builder should be equal to the glTF attribute count because we + // do not remap the slots. + VertexBuffer::Builder vbb; + vbb.bufferCount(inPrim->attributes_count); + + for (int slot = 0; slot < inPrim->attributes_count; slot++) { + const cgltf_attribute& inputAttribute = inPrim->attributes[slot]; + const cgltf_accessor* inputAccessor = inputAttribute.data; + + // At a minimum, surface orientation requires normals to be present in the source data. + // Here we re-purpose the normals slot to point to the quats that get computed later. + if (inputAttribute.type == cgltf_attribute_type_normal) { + vbb.attribute(VertexAttribute::TANGENTS, slot, VertexBuffer::AttributeType::SHORT4); + vbb.normalized(VertexAttribute::TANGENTS); + continue; + } + + // The glTF tangent data is ignored here, but honored in ResourceLoader. + if (inputAttribute.type == cgltf_attribute_type_tangent) { + continue; + } + + // Translate the cgltf attribute enum into a Filament enum and ignore all uv sets + // that do not have entries in the mapping table. + VertexAttribute semantic; + if (!getVertexAttrType(inputAttribute.type, &semantic)) { + utils::slog.e << "Unrecognized vertex semantic." << utils::io::endl; + return false; + } + UvSet uvset = uvmap[inputAttribute.index]; + if (inputAttribute.type == cgltf_attribute_type_texcoord) { + switch (uvset) { + case UV0: + semantic = VertexAttribute::UV0; + break; + case UV1: + semantic = VertexAttribute::UV1; + break; + case UNUSED: + // It is perfectly acceptable to drop unused texture coordinate sets. In fact + // this can occur quite frequently, e.g. if the material has attached textures. + continue; + } + } + + // This will needlessly set the same vertex count multiple times, which should be fine. + vbb.vertexCount(inputAccessor->count); + + // The positions accessor is required to have min/max properties, use them to expand + // the bounding box for this primitive. + if (inputAttribute.type == cgltf_attribute_type_position) { + const float* minp = &inputAccessor->min[0]; + const float* maxp = &inputAccessor->max[0]; + outPrim->aabb.min = min(outPrim->aabb.min, float3(minp[0], minp[1], minp[2])); + outPrim->aabb.max = max(outPrim->aabb.max, float3(maxp[0], maxp[1], maxp[2])); + } + + VertexBuffer::AttributeType atype; + if (!getElementType(inputAccessor->type, inputAccessor->component_type, &atype)) { + slog.e << "Unsupported accessor type." << io::endl; + return false; + } + + if (inputAccessor->is_sparse) { + slog.e << "Sparse accessors not yet supported." << io::endl; + return false; + } + + // The cgltf library provides a stride value for all accessors, even though they do not + // exist in the glTF file. It is computed from the type and the stride of the buffer view. + // As a convenience, cgltf also replaces zero (default) stride with the actual stride. + vbb.attribute(semantic, slot, atype, 0, inputAccessor->stride); + + if (inputAccessor->normalized) { + vbb.normalized(semantic); + } + } + + VertexBuffer* vertices = mResult->mPrimMap[inPrim] = vbb.build(*mEngine); + mResult->mVertexBuffers.push_back(vertices); + + for (int slot = 0; slot < inPrim->attributes_count; slot++) { + const cgltf_attribute& inputAttribute = inPrim->attributes[slot]; + const cgltf_accessor* inputAccessor = inputAttribute.data; + const cgltf_buffer_view* bv = inputAccessor->buffer_view; + if (inputAttribute.type == cgltf_attribute_type_normal || + inputAttribute.type == cgltf_attribute_type_tangent) { + continue; + } + if (inputAttribute.type == cgltf_attribute_type_texcoord && + uvmap[inputAttribute.index] == UNUSED) { + continue; + } + mResult->mBufferBindings.emplace_back(BufferBinding { + .uri = bv->buffer->uri, + .totalSize = (uint32_t) bv->buffer->size, + .bufferIndex = slot, + .offset = computeBindingOffset(inputAccessor), + .size = computeBindingSize(inputAccessor), + .data = &bv->buffer->data, + .vertexBuffer = vertices, + .indexBuffer = nullptr, + .convertBytesToShorts = false, + .generateTrivialIndices = false + }); + } + + outPrim->indices = indices; + outPrim->vertices = vertices; + return true; +} + +MaterialInstance* FAssetLoader::createMaterialInstance(const cgltf_material* inputMat, + UvMap* uvmap, bool vertexColor) { + intptr_t key = ((intptr_t) inputMat) ^ (vertexColor ? 1 : 0); + auto iter = mMatInstanceCache.find(key); + if (iter != mMatInstanceCache.end()) { + return iter->second; + } + + // The default glTF material is non-lit black. + if (inputMat == nullptr) { + MaterialKey matkey { + .unlit = true + }; + Material* mat = mMaterials.getOrCreateMaterial(&matkey, uvmap, "default"); + MaterialInstance* mi = mat->createInstance(); + mResult->mMaterialInstances.push_back(mi); + return mMatInstanceCache[0] = mi; + } + + if (inputMat->has_pbr_specular_glossiness) { + slog.e << "KHR_materials_pbrSpecularGlossiness is not supported." << io::endl; + } + + auto pbrConfig = inputMat->pbr_metallic_roughness; + bool hasTextureTransforms = pbrConfig.base_color_texture.has_transform || + pbrConfig.metallic_roughness_texture.has_transform || + inputMat->normal_texture.has_transform || + inputMat->occlusion_texture.has_transform || + inputMat->emissive_texture.has_transform; + + MaterialKey matkey { + .doubleSided = (bool) inputMat->double_sided, + .unlit = (bool) inputMat->unlit, + .hasVertexColors = vertexColor, + .hasBaseColorTexture = pbrConfig.base_color_texture.texture, + .hasMetallicRoughnessTexture = pbrConfig.metallic_roughness_texture.texture, + .hasNormalTexture = inputMat->normal_texture.texture, + .hasOcclusionTexture = inputMat->occlusion_texture.texture, + .hasEmissiveTexture = inputMat->emissive_texture.texture, + .alphaMode = AlphaMode::OPAQUE, + .alphaMaskThreshold = 0.5f, + .baseColorUV = (uint8_t) pbrConfig.base_color_texture.texcoord, + .metallicRoughnessUV = (uint8_t) pbrConfig.metallic_roughness_texture.texcoord, + .emissiveUV = (uint8_t) inputMat->emissive_texture.texcoord, + .aoUV = (uint8_t) inputMat->occlusion_texture.texcoord, + .normalUV = (uint8_t) inputMat->normal_texture.texcoord, + .hasTextureTransforms = hasTextureTransforms + }; + + switch (inputMat->alpha_mode) { + case cgltf_alpha_mode_opaque: + matkey.alphaMode = AlphaMode::OPAQUE; + break; + case cgltf_alpha_mode_mask: + matkey.alphaMode = AlphaMode::MASKED; + matkey.alphaMaskThreshold = inputMat->alpha_cutoff; + break; + case cgltf_alpha_mode_blend: + matkey.alphaMode = AlphaMode::TRANSPARENT; + break; + } + + // This not only creates (or fetches) a material, it modifies the material key according to + // our rendering constraints. For example, Filament only supports 2 sets of texture coordinates. + Material* mat = mMaterials.getOrCreateMaterial(&matkey, uvmap, inputMat->name); + + // Create an instance of the material that has a unique set of texture bindings etc. + MaterialInstance* mi = mat->createInstance(); + mResult->mMaterialInstances.push_back(mi); + + const float* e = &inputMat->emissive_factor[0]; + mi->setParameter("emissiveFactor", float3(e[0], e[1], e[2])); + mi->setParameter("normalScale", inputMat->normal_texture.scale); + mi->setParameter("aoStrength", inputMat->occlusion_texture.scale); + + const float* c = &pbrConfig.base_color_factor[0]; + mi->setParameter("baseColorFactor", float4(c[0], c[1], c[2], c[3])); + mi->setParameter("metallicFactor", pbrConfig.metallic_factor); + mi->setParameter("roughnessFactor", pbrConfig.roughness_factor); + + if (matkey.hasBaseColorTexture) { + addTextureBinding(mi, "baseColorMap", pbrConfig.base_color_texture.texture, true); + if (matkey.hasTextureTransforms) { + const cgltf_texture_transform& uvt = pbrConfig.base_color_texture.transform; + auto uvmat = matrixFromUvTransform(uvt.offset, uvt.rotation, uvt.scale); + mi->setParameter("baseColorUvMatrix", uvmat); + } + } + + if (matkey.hasMetallicRoughnessTexture) { + addTextureBinding(mi, "metallicRoughnessMap", + pbrConfig.metallic_roughness_texture.texture, false); + if (matkey.hasTextureTransforms) { + const cgltf_texture_transform& uvt = pbrConfig.metallic_roughness_texture.transform; + auto uvmat = matrixFromUvTransform(uvt.offset, uvt.rotation, uvt.scale); + mi->setParameter("metallicRoughnessUvMatrix", uvmat); + } + } + + if (matkey.hasNormalTexture) { + addTextureBinding(mi, "normalMap", inputMat->normal_texture.texture, false); + if (matkey.hasTextureTransforms) { + const cgltf_texture_transform& uvt = inputMat->normal_texture.transform; + auto uvmat = matrixFromUvTransform(uvt.offset, uvt.rotation, uvt.scale); + mi->setParameter("normalUvMatrix", uvmat); + } + } + + if (matkey.hasOcclusionTexture) { + addTextureBinding(mi, "occlusionMap", inputMat->occlusion_texture.texture, false); + if (matkey.hasTextureTransforms) { + const cgltf_texture_transform& uvt = inputMat->occlusion_texture.transform; + auto uvmat = matrixFromUvTransform(uvt.offset, uvt.rotation, uvt.scale); + mi->setParameter("occlusionUvMatrix", uvmat); + } + } + + if (matkey.hasEmissiveTexture) { + addTextureBinding(mi, "emissiveMap", inputMat->emissive_texture.texture, true); + if (matkey.hasTextureTransforms) { + const cgltf_texture_transform& uvt = inputMat->emissive_texture.transform; + auto uvmat = matrixFromUvTransform(uvt.offset, uvt.rotation, uvt.scale); + mi->setParameter("emissiveUvMatrix", uvmat); + } + } + + return mMatInstanceCache[key] = mi; +} + +void FAssetLoader::addTextureBinding(MaterialInstance* materialInstance, const char* parameterName, + const cgltf_texture* srcTexture, bool srgb) { + if (!srcTexture->image) { + slog.w << "Texture is missing image (" << srcTexture->name << ")." << io::endl; + return; + } + TextureSampler dstSampler; + auto srcSampler = srcTexture->sampler; + if (srcSampler) { + dstSampler.setWrapModeS(getWrapMode(srcSampler->wrap_s)); + dstSampler.setWrapModeT(getWrapMode(srcSampler->wrap_t)); + dstSampler.setMagFilter(getMagFilter(srcSampler->mag_filter)); + dstSampler.setMinFilter(getMinFilter(srcSampler->min_filter)); + } else { + // These defaults are stipulated by the spec: + dstSampler.setWrapModeS(TextureSampler::WrapMode::REPEAT); + dstSampler.setWrapModeT(TextureSampler::WrapMode::REPEAT); + + // These defaults are up the implementation but since we generate mipmaps unconditionally, + // we might as well use them. In practice the conformance models look awful without + // using mipmapping by default. + dstSampler.setMagFilter(TextureSampler::MagFilter::LINEAR); + dstSampler.setMinFilter(TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR); + } + auto bv = srcTexture->image->buffer_view; + mResult->mTextureBindings.push_back(TextureBinding { + .uri = srcTexture->image->uri, + .totalSize = uint32_t(bv ? bv->buffer->size : 0), + .mimeType = srcTexture->image->mime_type, + .data = bv ? &bv->buffer->data : nullptr, + .materialInstance = materialInstance, + .materialParameter = parameterName, + .sampler = dstSampler, + .srgb = srgb + }); +} + +void FAssetLoader::importSkinningData(Skin& dstSkin, const cgltf_skin& srcSkin) { + if (srcSkin.name) { + dstSkin.name = srcSkin.name; + } + dstSkin.joints.resize(srcSkin.joints_count); + const auto& nodeMap = mResult->mNodeMap; + for (cgltf_size i = 0, len = srcSkin.joints_count; i < len; ++i) { + dstSkin.joints[i] = nodeMap.at(srcSkin.joints[i]); + } +} + +bool FAssetLoader::primitiveHasVertexColor(const cgltf_primitive* inPrim) const { + for (int slot = 0; slot < inPrim->attributes_count; slot++) { + const cgltf_attribute& inputAttribute = inPrim->attributes[slot]; + if (inputAttribute.type == cgltf_attribute_type_color) { + return true; + } + } + return false; +} + +AssetLoader* AssetLoader::create(Engine* engine, NameComponentManager* names) { + return new FAssetLoader(engine, names); +} + +void AssetLoader::destroy(AssetLoader** loader) { + delete *loader; + *loader = nullptr; +} + +FilamentAsset* AssetLoader::createAssetFromJson(uint8_t const* bytes, uint32_t nbytes) { + return upcast(this)->createAssetFromJson(bytes, nbytes); +} + +FilamentAsset* AssetLoader::createAssetFromBinary(uint8_t const* bytes, uint32_t nbytes) { + return upcast(this)->createAssetFromBinary(bytes, nbytes); +} + +void AssetLoader::destroyAsset(const FilamentAsset* asset) { + upcast(this)->destroyAsset(upcast(asset)); +} + +void AssetLoader::castShadowsByDefault(bool enable) { + upcast(this)->castShadowsByDefault(enable); +} + +void AssetLoader::receiveShadowsByDefault(bool enable) { + upcast(this)->receiveShadowsByDefault(enable); +} + +size_t AssetLoader::getMaterialsCount() const noexcept { + return upcast(this)->getMaterialsCount(); +} + +const Material* const* AssetLoader::getMaterials() const noexcept { + return upcast(this)->getMaterials(); +} + +void AssetLoader::destroyMaterials() { + upcast(this)->destroyMaterials(); +} + +} // namespace gltfio diff --git a/libs/gltfio/src/FFilamentAsset.h b/libs/gltfio/src/FFilamentAsset.h new file mode 100644 index 0000000000..62b323eda6 --- /dev/null +++ b/libs/gltfio/src/FFilamentAsset.h @@ -0,0 +1,175 @@ +/* + * 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 GLTFIO_FFILAMENTASSET_H +#define GLTFIO_FFILAMENTASSET_H + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include + +#include "upcast.h" + +#include + +#include +#include +#include + +namespace gltfio { +namespace details { + +struct Skin { + std::string name; + std::vector inverseBindMatrices; + std::vector joints; + std::vector targets; +}; + +struct FFilamentAsset : public FilamentAsset { + FFilamentAsset(filament::Engine* engine) : mEngine(engine) {} + + ~FFilamentAsset() { + releaseSourceData(); + delete mAnimator; + mEngine->destroy(mRoot); + for (auto entity : mEntities) { + mEngine->destroy(entity); + } + for (auto mi : mMaterialInstances) { + mEngine->destroy(mi); + } + for (auto vb : mVertexBuffers) { + mEngine->destroy(vb); + } + for (auto ib : mIndexBuffers) { + mEngine->destroy(ib); + } + for (auto tx : mTextures) { + mEngine->destroy(tx); + } + } + + size_t getEntityCount() const noexcept { + return mEntities.size(); + } + + const utils::Entity* getEntities() const noexcept { + return mEntities.data(); + } + + utils::Entity getRoot() const noexcept { + return mRoot; + } + + size_t getMaterialInstanceCount() const noexcept { + return mMaterialInstances.size(); + } + + const filament::MaterialInstance* const* getMaterialInstances() const noexcept { + return mMaterialInstances.data(); + } + + size_t getBufferBindingCount() const noexcept { + return mBufferBindings.size(); + } + + const BufferBinding* getBufferBindings() const noexcept { + return mBufferBindings.data(); + } + + size_t getTextureBindingCount() const noexcept { + return mTextureBindings.size(); + } + + const TextureBinding* getTextureBindings() const noexcept { + return mTextureBindings.data(); + } + + filament::Aabb getBoundingBox() const noexcept { + return mBoundingBox; + } + + Animator* getAnimator() noexcept { + if (!mAnimator) { + mAnimator = new Animator(this); + } + return mAnimator; + } + + void releaseSourceData() noexcept { + mBufferBindings.clear(); + mBufferBindings.shrink_to_fit(); + mTextureBindings.clear(); + mTextureBindings.shrink_to_fit(); + mNodeMap.clear(); + mPrimMap.clear(); + releaseSourceAsset(); + } + + void acquireSourceAsset() { + ++mSourceAssetRefCount; + } + + void releaseSourceAsset() { + if (--mSourceAssetRefCount == 0) { + cgltf_free((cgltf_data*) mSourceAsset); + mSourceAsset = nullptr; + } + } + + filament::Engine* mEngine; + std::vector mEntities; + std::vector mMaterialInstances; + std::vector mVertexBuffers; + std::vector mIndexBuffers; + std::vector mTextures; + filament::Aabb mBoundingBox; + utils::Entity mRoot; + std::vector mSkins; + Animator* mAnimator = nullptr; + int mSourceAssetRefCount = 0; + + /** @{ + * Transient source data that can freed via releaseSourceData(). */ + std::vector mBufferBindings; + std::vector mTextureBindings; + const cgltf_data* mSourceAsset = nullptr; + tsl::robin_map mNodeMap; + tsl::robin_map mPrimMap; + /** @} */ +}; + +FILAMENT_UPCAST(FilamentAsset) + +} // namespace details +} // namespace gltfio + +#endif // GLTFIO_FFILAMENTASSET_H diff --git a/libs/gltfio/src/FilamentAsset.cpp b/libs/gltfio/src/FilamentAsset.cpp new file mode 100644 index 0000000000..d3e9dadd43 --- /dev/null +++ b/libs/gltfio/src/FilamentAsset.cpp @@ -0,0 +1,74 @@ +/* + * 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 "FFilamentAsset.h" + +using namespace filament; +using namespace utils; + +namespace gltfio { + +using namespace details; + +size_t FilamentAsset::getEntityCount() const noexcept { + return upcast(this)->getEntityCount(); +} + +const Entity* FilamentAsset::getEntities() const noexcept { + return upcast(this)->getEntities(); +} + +Entity FilamentAsset::getRoot() const noexcept { + return upcast(this)->getRoot(); +} + +size_t FilamentAsset::getMaterialInstanceCount() const noexcept { + return upcast(this)->getMaterialInstanceCount(); +} + +const MaterialInstance* const* FilamentAsset::getMaterialInstances() const noexcept { + return upcast(this)->getMaterialInstances(); +} + +size_t FilamentAsset::getBufferBindingCount() const noexcept { + return upcast(this)->getBufferBindingCount(); +} + +const BufferBinding* FilamentAsset::getBufferBindings() const noexcept { + return upcast(this)->getBufferBindings(); +} + +size_t FilamentAsset::getTextureBindingCount() const noexcept { + return upcast(this)->getTextureBindingCount(); +} + +const TextureBinding* FilamentAsset::getTextureBindings() const noexcept { + return upcast(this)->getTextureBindings(); +} + +filament::Aabb FilamentAsset::getBoundingBox() const noexcept { + return upcast(this)->getBoundingBox(); +} + +Animator* FilamentAsset::getAnimator() noexcept { + return upcast(this)->getAnimator(); +} + +void FilamentAsset::releaseSourceData() noexcept { + return upcast(this)->releaseSourceData(); +} + +} // namespace gltfio diff --git a/libs/gltfio/src/GltfEnums.h b/libs/gltfio/src/GltfEnums.h new file mode 100644 index 0000000000..ec5bc506a0 --- /dev/null +++ b/libs/gltfio/src/GltfEnums.h @@ -0,0 +1,233 @@ +/* + * 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 GLTFIO_GLTFENUMS_H +#define GLTFIO_GLTFENUMS_H + +#include +#include +#include +#include + +#include + +#define GL_NEAREST 0x2600 +#define GL_LINEAR 0x2601 +#define GL_NEAREST_MIPMAP_NEAREST 0x2700 +#define GL_LINEAR_MIPMAP_NEAREST 0x2701 +#define GL_NEAREST_MIPMAP_LINEAR 0x2702 +#define GL_LINEAR_MIPMAP_LINEAR 0x2703 +#define GL_REPEAT 0x2901 +#define GL_MIRRORED_REPEAT 0x8370 +#define GL_CLAMP_TO_EDGE 0x812F + +inline filament::TextureSampler::WrapMode getWrapMode(cgltf_int wrap) { + switch (wrap) { + case GL_REPEAT: + return filament::TextureSampler::WrapMode::REPEAT; + case GL_MIRRORED_REPEAT: + return filament::TextureSampler::WrapMode::MIRRORED_REPEAT; + case GL_CLAMP_TO_EDGE: + return filament::TextureSampler::WrapMode::CLAMP_TO_EDGE; + } + return filament::TextureSampler::WrapMode::REPEAT; +} + +inline filament::TextureSampler::MinFilter getMinFilter(cgltf_int minFilter) { + switch (minFilter) { + case GL_NEAREST: + return filament::TextureSampler::MinFilter::NEAREST; + case GL_LINEAR: + return filament::TextureSampler::MinFilter::LINEAR; + case GL_NEAREST_MIPMAP_NEAREST: + return filament::TextureSampler::MinFilter::NEAREST_MIPMAP_NEAREST; + case GL_LINEAR_MIPMAP_NEAREST: + return filament::TextureSampler::MinFilter::LINEAR_MIPMAP_NEAREST; + case GL_NEAREST_MIPMAP_LINEAR: + return filament::TextureSampler::MinFilter::NEAREST_MIPMAP_LINEAR; + case GL_LINEAR_MIPMAP_LINEAR: + return filament::TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR; + } + return filament::TextureSampler::MinFilter::NEAREST; +} + +inline filament::TextureSampler::MagFilter getMagFilter(cgltf_int magFilter) { + switch (magFilter) { + case GL_NEAREST: + return filament::TextureSampler::MagFilter::NEAREST; + case GL_LINEAR: + return filament::TextureSampler::MagFilter::LINEAR; + } + return filament::TextureSampler::MagFilter::NEAREST; +} + +inline bool getVertexAttrType(cgltf_attribute_type atype, filament::VertexAttribute* attrType) { + switch (atype) { + case cgltf_attribute_type_position: + *attrType = filament::VertexAttribute::POSITION; + return true; + case cgltf_attribute_type_texcoord: + *attrType = filament::VertexAttribute::UV0; + return true; + case cgltf_attribute_type_color: + *attrType = filament::VertexAttribute::COLOR; + return true; + case cgltf_attribute_type_joints: + *attrType = filament::VertexAttribute::BONE_INDICES; + return true; + case cgltf_attribute_type_weights: + *attrType = filament::VertexAttribute::BONE_WEIGHTS; + return true; + case cgltf_attribute_type_normal: + case cgltf_attribute_type_tangent: + default: + return false; + } +} + +inline bool getIndexType(cgltf_component_type ctype, filament::IndexBuffer::IndexType* itype) { + switch (ctype) { + case cgltf_component_type_r_8u: + case cgltf_component_type_r_16u: + *itype = filament::IndexBuffer::IndexType::USHORT; + return true; + case cgltf_component_type_r_32u: + *itype = filament::IndexBuffer::IndexType::UINT; + return true; + default: + break; + } + return false; +} + +inline bool getPrimitiveType(cgltf_primitive_type in, + filament::RenderableManager::PrimitiveType* out) { + switch (in) { + case cgltf_primitive_type_points: + *out = filament::RenderableManager::PrimitiveType::POINTS; + return true; + case cgltf_primitive_type_lines: + *out = filament::RenderableManager::PrimitiveType::LINES; + return true; + case cgltf_primitive_type_triangles: + *out = filament::RenderableManager::PrimitiveType::TRIANGLES; + return true; + case cgltf_primitive_type_line_loop: + case cgltf_primitive_type_line_strip: + case cgltf_primitive_type_triangle_strip: + case cgltf_primitive_type_triangle_fan: + return false; + } + return false; +} + +inline bool getElementType(cgltf_type type, cgltf_component_type ctype, + filament::VertexBuffer::AttributeType* atype) { + switch (type) { + case cgltf_type_scalar: + switch (ctype) { + case cgltf_component_type_r_8: + *atype = filament::VertexBuffer::AttributeType::BYTE; + return true; + case cgltf_component_type_r_8u: + *atype = filament::VertexBuffer::AttributeType::UBYTE; + return true; + case cgltf_component_type_r_16: + *atype = filament::VertexBuffer::AttributeType::SHORT; + return true; + case cgltf_component_type_r_16u: + *atype = filament::VertexBuffer::AttributeType::USHORT; + return true; + case cgltf_component_type_r_32u: + *atype = filament::VertexBuffer::AttributeType::UINT; + return true; + case cgltf_component_type_r_32f: + *atype = filament::VertexBuffer::AttributeType::FLOAT; + return true; + default: + return false; + } + break; + case cgltf_type_vec2: + switch (ctype) { + case cgltf_component_type_r_8: + *atype = filament::VertexBuffer::AttributeType::BYTE2; + return true; + case cgltf_component_type_r_8u: + *atype = filament::VertexBuffer::AttributeType::UBYTE2; + return true; + case cgltf_component_type_r_16: + *atype = filament::VertexBuffer::AttributeType::SHORT2; + return true; + case cgltf_component_type_r_16u: + *atype = filament::VertexBuffer::AttributeType::USHORT2; + return true; + case cgltf_component_type_r_32f: + *atype = filament::VertexBuffer::AttributeType::FLOAT2; + return true; + default: + return false; + } + break; + case cgltf_type_vec3: + switch (ctype) { + case cgltf_component_type_r_8: + *atype = filament::VertexBuffer::AttributeType::BYTE3; + return true; + case cgltf_component_type_r_8u: + *atype = filament::VertexBuffer::AttributeType::UBYTE3; + return true; + case cgltf_component_type_r_16: + *atype = filament::VertexBuffer::AttributeType::SHORT3; + return true; + case cgltf_component_type_r_16u: + *atype = filament::VertexBuffer::AttributeType::USHORT3; + return true; + case cgltf_component_type_r_32f: + *atype = filament::VertexBuffer::AttributeType::FLOAT3; + return true; + default: + return false; + } + break; + case cgltf_type_vec4: + switch (ctype) { + case cgltf_component_type_r_8: + *atype = filament::VertexBuffer::AttributeType::BYTE4; + return true; + case cgltf_component_type_r_8u: + *atype = filament::VertexBuffer::AttributeType::UBYTE4; + return true; + case cgltf_component_type_r_16: + *atype = filament::VertexBuffer::AttributeType::SHORT4; + return true; + case cgltf_component_type_r_16u: + *atype = filament::VertexBuffer::AttributeType::USHORT4; + return true; + case cgltf_component_type_r_32f: + *atype = filament::VertexBuffer::AttributeType::FLOAT4; + return true; + default: + return false; + } + break; + default: + return false; + } + return false; +} + +#endif // GLTFIO_GLTFENUMS_H diff --git a/libs/gltfio/src/MaterialGenerator.cpp b/libs/gltfio/src/MaterialGenerator.cpp new file mode 100644 index 0000000000..ce976cdafa --- /dev/null +++ b/libs/gltfio/src/MaterialGenerator.cpp @@ -0,0 +1,316 @@ +/* + * 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 "MaterialGenerator.h" + +#include + +#include + +#include + +using namespace filamat; +using namespace filament; +using namespace utils; + +namespace gltfio { +namespace details { + +bool MaterialGenerator::EqualFn::operator()(const MaterialKey& k1, const MaterialKey& k2) const { + return + (k1.doubleSided == k2.doubleSided) && + (k1.unlit == k2.unlit) && + (k1.hasVertexColors == k2.hasVertexColors) && + (k1.hasBaseColorTexture == k2.hasBaseColorTexture) && + (k1.hasMetallicRoughnessTexture == k2.hasMetallicRoughnessTexture) && + (k1.hasNormalTexture == k2.hasNormalTexture) && + (k1.hasOcclusionTexture == k2.hasOcclusionTexture) && + (k1.hasEmissiveTexture == k2.hasEmissiveTexture) && + (k1.alphaMode == k2.alphaMode) && + (k1.baseColorUV == k2.baseColorUV) && + (k1.metallicRoughnessUV == k2.metallicRoughnessUV) && + (k1.emissiveUV == k2.emissiveUV) && + (k1.aoUV == k2.aoUV) && + (k1.normalUV == k2.normalUV) && + (k1.alphaMaskThreshold == k2.alphaMaskThreshold); +} + +MaterialGenerator::MaterialGenerator(Engine* engine) : mEngine(engine) { + MaterialBuilder::init(); +} + +size_t MaterialGenerator::getMaterialsCount() const noexcept { + return mMaterials.size(); +} + +const Material* const* MaterialGenerator::getMaterials() const noexcept { + return mMaterials.data(); +} + +void MaterialGenerator::destroyMaterials() { + for (auto& iter : mCache) { + mEngine->destroy(iter.second); + } + mMaterials.clear(); + mCache.clear(); +} + +static std::string shaderFromKey(const MaterialKey& config, const UvMap& uvmap) { + const auto normalUV = std::to_string(uvmap[config.normalUV] - 1); + const auto baseColorUV = std::to_string(uvmap[config.baseColorUV] - 1); + const auto metallicRoughnessUV = std::to_string(uvmap[config.metallicRoughnessUV] - 1); + const auto emissiveUV = std::to_string(uvmap[config.emissiveUV] - 1); + const auto aoUV = std::to_string(uvmap[config.aoUV] - 1); + + std::string shader = R"SHADER( + + // Sigh, assimp flips texture coords in its glTF2Importer, but we're not using assimp so we + // need to flip them here. + #if defined(HAS_ATTRIBUTE_UV0) + float2 uv0() { vec2 uv = getUV0(); uv.y = 1.0 - uv.y; return uv; } + #endif + #if defined(HAS_ATTRIBUTE_UV1) + float2 uv1() { vec2 uv = getUV1(); uv.y = 1.0 - uv.y; return uv; } + #endif + + void material(inout MaterialInputs material) { + )SHADER"; + + if (config.hasNormalTexture && !config.unlit) { + shader += "float2 normalUV = uv" + normalUV + "();\n"; + if (config.hasTextureTransforms) { + shader += "normalUV = (vec3(normalUV, 1.0) * materialParams.normalUvMatrix).xy;\n"; + } + shader += R"SHADER( + material.normal = texture(materialParams_normalMap, normalUV).xyz * 2.0 - 1.0; + material.normal.y = -material.normal.y; + material.normal.xy *= materialParams.normalScale; + )SHADER"; + } + + shader += R"SHADER( + prepareMaterial(material); + material.baseColor = materialParams.baseColorFactor; + )SHADER"; + + if (config.hasBaseColorTexture) { + shader += "float2 baseColorUV = uv" + baseColorUV + "();\n"; + if (config.hasTextureTransforms) { + shader += "baseColorUV = (vec3(baseColorUV, 1.0) * materialParams.baseColorUvMatrix).xy;\n"; + } + shader += R"SHADER( + material.baseColor *= texture(materialParams_baseColorMap, baseColorUV); + )SHADER"; + } + + if (config.alphaMode == AlphaMode::TRANSPARENT) { + shader += R"SHADER( + material.baseColor.rgb *= material.baseColor.a; + )SHADER"; + } + + if (config.hasVertexColors) { + shader += "material.baseColor *= getColor();\n"; + } + + if (!config.unlit) { + shader += R"SHADER( + material.roughness = materialParams.roughnessFactor; + material.metallic = materialParams.metallicFactor; + material.emissive.rgb = materialParams.emissiveFactor.rgb; + )SHADER"; + if (config.hasMetallicRoughnessTexture) { + shader += "float2 metallicRoughnessUV = uv" + metallicRoughnessUV + "();\n"; + if (config.hasTextureTransforms) { + shader += "metallicRoughnessUV = (vec3(metallicRoughnessUV, 1.0) * materialParams.metallicRoughnessUvMatrix).xy;\n"; + } + shader += R"SHADER( + vec4 roughness = texture(materialParams_metallicRoughnessMap, metallicRoughnessUV); + material.roughness *= roughness.g; + material.metallic *= roughness.b; + )SHADER"; + } + if (config.hasOcclusionTexture) { + shader += "float2 aoUV = uv" + aoUV + "();\n"; + if (config.hasTextureTransforms) { + shader += "aoUV = (vec3(aoUV, 1.0) * materialParams.occlusionUvMatrix).xy;\n"; + } + shader += R"SHADER( + material.ambientOcclusion = texture(materialParams_occlusionMap, aoUV).r * + materialParams.aoStrength; + )SHADER"; + } + if (config.hasEmissiveTexture) { + shader += "float2 emissiveUV = uv" + emissiveUV + "();\n"; + if (config.hasTextureTransforms) { + shader += "aoUV = (vec3(emissiveUV, 1.0) * materialParams.emissiveUvMatrix).xy;\n"; + } + shader += R"SHADER( + material.emissive.rgb *= texture(materialParams_emissiveMap, emissiveUV).rgb; + material.emissive.a = 3.0; + )SHADER"; + } + } + + shader += "}\n"; + return shader; +} + +// Filament supports up to 2 UV sets. glTF has arbitrary texcoord set indices, but it allows +// implementations to support only 2 simultaneous sets. Here we build a mapping table with 1-based +// indices where 0 means unused. Note that the order in which we drop textures can affect the look +// of certain assets. This "order of degradation" is stipulated by the glTF 2.0 specification. +static void constrainMaterial(MaterialKey* key, UvMap* uvmap) { + const int MAX_INDEX = 2; + UvMap retval {}; + int index = 1; + if (key->hasBaseColorTexture) { + retval[key->baseColorUV] = (UvSet) index++; + } + if (key->hasMetallicRoughnessTexture && retval[key->metallicRoughnessUV] == UNUSED) { + retval[key->metallicRoughnessUV] = (UvSet) index++; + } + if (key->hasNormalTexture && retval[key->normalUV] == UNUSED) { + if (index > MAX_INDEX) { + key->hasNormalTexture = false; + } else { + retval[key->normalUV] = (UvSet) index++; + } + } + if (key->hasOcclusionTexture && retval[key->aoUV] == UNUSED) { + if (index > MAX_INDEX) { + key->hasOcclusionTexture = false; + } else { + retval[key->aoUV] = (UvSet) index++; + } + } + if (key->hasEmissiveTexture && retval[key->emissiveUV] == UNUSED) { + if (index > MAX_INDEX) { + key->hasEmissiveTexture = false; + } else { + retval[key->emissiveUV] = (UvSet) index++; + } + } + *uvmap = retval; +} + +static Material* createMaterial(Engine* engine, const MaterialKey& config, const UvMap& uvmap, + const char* name) { + using CullingMode = MaterialBuilder::CullingMode; + std::string shader = shaderFromKey(config, uvmap); + MaterialBuilder builder = MaterialBuilder() + .name(name) + .material(shader.c_str()) + .culling(config.doubleSided ? CullingMode::NONE : CullingMode::BACK) + .doubleSided(config.doubleSided); + + auto uvset = (uint8_t*) &uvmap.front(); + static_assert(std::tuple_size::value == 8, "Badly sized uvset."); + int numTextures = std::max({ + uvset[0], uvset[1], uvset[2], uvset[3], + uvset[4], uvset[5], uvset[6], uvset[7], + }); + if (numTextures > 0) { + builder.require(VertexAttribute::UV0); + } + if (numTextures > 1) { + builder.require(VertexAttribute::UV1); + } + + // BASE COLOR + builder.parameter(MaterialBuilder::UniformType::FLOAT4, "baseColorFactor"); + if (config.hasBaseColorTexture) { + builder.parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "baseColorMap"); + if (config.hasTextureTransforms) { + builder.parameter(MaterialBuilder::UniformType::MAT3, "baseColorUvMatrix"); + } + } + if (config.hasVertexColors) { + builder.require(VertexAttribute::COLOR); + } + + // METALLIC-ROUGHNESS + builder.parameter(MaterialBuilder::UniformType::FLOAT, "metallicFactor"); + builder.parameter(MaterialBuilder::UniformType::FLOAT, "roughnessFactor"); + if (config.hasMetallicRoughnessTexture) { + builder.parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "metallicRoughnessMap"); + if (config.hasTextureTransforms) { + builder.parameter(MaterialBuilder::UniformType::MAT3, "metallicRoughnessUvMatrix"); + } + } + + // NORMAL MAP + // In the glTF spec normalScale is in normalTextureInfo; in cgltf it is part of texture_view. + builder.parameter(MaterialBuilder::UniformType::FLOAT, "normalScale"); + if (config.hasNormalTexture) { + builder.parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "normalMap"); + if (config.hasTextureTransforms) { + builder.parameter(MaterialBuilder::UniformType::MAT3, "normalUvMatrix"); + } + } + + // AMBIENT OCCLUSION + // In the glTF spec aoStrength is in occlusionTextureInfo; in cgltf it is part of texture_view. + builder.parameter(MaterialBuilder::UniformType::FLOAT, "aoStrength"); + if (config.hasOcclusionTexture) { + builder.parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "occlusionMap"); + if (config.hasTextureTransforms) { + builder.parameter(MaterialBuilder::UniformType::MAT3, "occlusionUvMatrix"); + } + } + + // EMISSIVE + builder.parameter(MaterialBuilder::UniformType::FLOAT3, "emissiveFactor"); + if (config.hasEmissiveTexture) { + builder.parameter(MaterialBuilder::SamplerType::SAMPLER_2D, "emissiveMap"); + if (config.hasTextureTransforms) { + builder.parameter(MaterialBuilder::UniformType::MAT3, "emissiveUvMatrix"); + } + } + + switch(config.alphaMode) { + case AlphaMode::MASKED: + builder.blending(MaterialBuilder::BlendingMode::MASKED); + builder.maskThreshold(config.alphaMaskThreshold); + break; + case AlphaMode::TRANSPARENT: + builder.blending(MaterialBuilder::BlendingMode::TRANSPARENT); + break; + default: + builder.blending(MaterialBuilder::BlendingMode::OPAQUE); + } + + builder.shading(config.unlit ? Shading::UNLIT : Shading::LIT); + + Package pkg = builder.build(); + return Material::Builder().package(pkg.getData(), pkg.getSize()).build(*engine); +} + +Material* MaterialGenerator::getOrCreateMaterial(MaterialKey* config, UvMap* uvmap, + const char* label) { + constrainMaterial(config, uvmap); + auto iter = mCache.find(*config); + if (iter == mCache.end()) { + Material* mat = createMaterial(mEngine, *config, *uvmap, label); + mCache.emplace(std::make_pair(*config, mat)); + mMaterials.push_back(mat); + return mat; + } + return iter->second; +} + +} // namespace details +} // namespace gltfio diff --git a/libs/gltfio/src/MaterialGenerator.h b/libs/gltfio/src/MaterialGenerator.h new file mode 100644 index 0000000000..7b6b4f8342 --- /dev/null +++ b/libs/gltfio/src/MaterialGenerator.h @@ -0,0 +1,91 @@ +/* + * 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 GLTFIO_MATERIALGENERATOR_H +#define GLTFIO_MATERIALGENERATOR_H + +#include +#include + +#include + +#include + +#include + +namespace gltfio { +namespace details { + +enum class AlphaMode : uint8_t { + OPAQUE, + MASKED, + TRANSPARENT +}; + +struct MaterialKey { + bool doubleSided : 1; + bool unlit : 1; + bool hasVertexColors : 1; + bool hasBaseColorTexture : 1; + bool hasMetallicRoughnessTexture : 1; + bool hasNormalTexture : 1; + bool hasOcclusionTexture : 1; + bool hasEmissiveTexture : 1; + AlphaMode alphaMode; + uint8_t baseColorUV; + uint8_t metallicRoughnessUV; + uint8_t emissiveUV; + uint8_t aoUV; + uint8_t normalUV; + float alphaMaskThreshold; + bool hasTextureTransforms; +}; + +// Define a mapping from a uv set index in the source asset to one of Filament's uv sets. +enum UvSet : uint8_t { UNUSED, UV0, UV1 }; +using UvMap = std::array; + +// The MaterialGenerator uses filamat to generate materials that each unconditionally make the +// minimum number of texture lookups. This complexity could be avoided if we were to use an +// ubershader approach, but this allows us to generate efficient and streamlined shaders that have +// no branching. +class MaterialGenerator final { +public: + MaterialGenerator(filament::Engine* engine); + + // Creates or fetches a compiled Filament material. The given configuration key might be mutated + // due to resource constraints. The second argument is populated with a small table that maps + // from a glTF uv index to a Filament uv index. The third argument is an optional tag that + // is not a part of the cache key. + filament::Material* getOrCreateMaterial(MaterialKey* config, UvMap* uvmap, + const char* label = "material"); + + size_t getMaterialsCount() const noexcept; + const filament::Material* const* getMaterials() const noexcept; + void destroyMaterials(); + +private: + using HashFn = utils::hash::MurmurHashFn; + struct EqualFn { bool operator()(const MaterialKey& k1, const MaterialKey& k2) const; }; + tsl::robin_map mCache; + std::vector mMaterials; + filament::Engine* mEngine; +}; + +} // namespace details +} // namespace gltfio + +#endif // GLTFIO_MATERIALGENERATOR_H diff --git a/libs/gltfio/src/ResourceLoader.cpp b/libs/gltfio/src/ResourceLoader.cpp new file mode 100644 index 0000000000..e068294cb5 --- /dev/null +++ b/libs/gltfio/src/ResourceLoader.cpp @@ -0,0 +1,412 @@ +/* + * 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 "FFilamentAsset.h" +#include "upcast.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#include + +#include + +#include + +#include + +using namespace filament; +using namespace filament::math; +using namespace utils; + +namespace gltfio { +namespace details { + +// The AssetPool tracks references to raw source data (cgltf hierarchies) and frees them +// appropriately. It releases all source assets only after the pending upload count is zero and the +// client has destroyed the ResourceLoader object. If the ResourceLoader is destroyed while uploads +// are still pending, then the AssetPool will stay alive until all uploads are complete. +class AssetPool { +public: + AssetPool() {} + ~AssetPool() { + for (auto asset : mAssets) { + asset->releaseSourceAsset(); + } + } + void addAsset(FFilamentAsset* asset) { + mAssets.push_back(asset); + asset->acquireSourceAsset(); + } + void addPendingUpload() { + ++mPendingUploads; + } + static void onLoadedResource(void* buffer, size_t size, void* user) { + auto pool = (AssetPool*) user; + if (--pool->mPendingUploads == 0 && pool->mLoaderDestroyed) { + delete pool; + } + } + void onLoaderDestroyed() { + if (mPendingUploads == 0) { + delete this; + } else { + mLoaderDestroyed = true; + } + } +private: + std::vector mAssets; + bool mLoaderDestroyed = false; + int mPendingUploads = 0; +}; + +} // namespace details + +using namespace details; + +ResourceLoader::ResourceLoader(const ResourceConfiguration& config) : mConfig(config), + mPool(new AssetPool) {} + +ResourceLoader::~ResourceLoader() { + mPool->onLoaderDestroyed(); +} + +static void importSkinningData(Skin& dstSkin, const cgltf_skin& srcSkin) { + const cgltf_accessor* srcMatrices = srcSkin.inverse_bind_matrices; + dstSkin.inverseBindMatrices.resize(srcSkin.joints_count); + if (srcMatrices) { + auto dstMatrices = (uint8_t*) dstSkin.inverseBindMatrices.data(); + auto srcBuffer = srcMatrices->buffer_view->buffer->data; + memcpy(dstMatrices, srcBuffer, srcSkin.joints_count * sizeof(mat4f)); + } +} + +static void convertBytesToShorts(uint16_t* dst, const uint8_t* src, size_t count) { + for (size_t i = 0; i < count; ++i) { + dst[i] = src[i]; + } +} + +static void generateTrivialIndices(uint32_t* dst, size_t numVertices) { + for (size_t i = 0; i < numVertices; ++i) { + dst[i] = i; + } +} + +bool ResourceLoader::loadResources(FilamentAsset* asset) { + FFilamentAsset* fasset = upcast(asset); + mPool->addAsset(fasset); + auto gltf = (cgltf_data*) fasset->mSourceAsset; + + // Read data from the file system and base64 URLs. + cgltf_options options {}; + cgltf_result result = cgltf_load_buffers(&options, gltf, mConfig.basePath.c_str()); + if (result != cgltf_result_success) { + slog.e << "Unable to load resources." << io::endl; + return false; + } + + // To be robust against the glTF conformance suite, we optionally ensure that skinning weights + // sum to 1.0 at every vertex. Note that if the same weights buffer is shared in multiple + // places, this will needlessly repeat the work. In the future we would like to remove this + // feature, and instead simply require correct models. See also: + // https://github.com/KhronosGroup/glTF-Sample-Models/issues/215 + if (mConfig.normalizeSkinningWeights) { + normalizeWeights(fasset); + } + + // Upload data to the GPU. + const BufferBinding* bindings = asset->getBufferBindings(); + for (size_t i = 0, n = asset->getBufferBindingCount(); i < n; ++i) { + auto bb = bindings[i]; + const uint8_t* data8 = bb.data ? (bb.offset + (const uint8_t*) *bb.data) : nullptr; + if (bb.vertexBuffer) { + mPool->addPendingUpload(); + VertexBuffer::BufferDescriptor bd(data8, bb.size, AssetPool::onLoadedResource, mPool); + bb.vertexBuffer->setBufferAt(*mConfig.engine, bb.bufferIndex, std::move(bd)); + } else if (bb.generateTrivialIndices) { + uint32_t* data32 = (uint32_t*) malloc(bb.size); + generateTrivialIndices(data32, bb.size / sizeof(uint32_t)); + auto callback = (IndexBuffer::BufferDescriptor::Callback) free; + IndexBuffer::BufferDescriptor bd(data32, bb.size, callback); + bb.indexBuffer->setBuffer(*mConfig.engine, std::move(bd)); + } else if (bb.convertBytesToShorts) { + size_t size16 = bb.size * 2; + uint16_t* data16 = (uint16_t*) malloc(size16); + convertBytesToShorts(data16, data8, bb.size); + auto callback = (IndexBuffer::BufferDescriptor::Callback) free; + IndexBuffer::BufferDescriptor bd(data16, size16, callback); + bb.indexBuffer->setBuffer(*mConfig.engine, std::move(bd)); + } else if (bb.indexBuffer) { + mPool->addPendingUpload(); + IndexBuffer::BufferDescriptor bd(data8, bb.size, AssetPool::onLoadedResource, mPool); + bb.indexBuffer->setBuffer(*mConfig.engine, std::move(bd)); + } + } + + // Copy over the inverse bind matrices to allow users to destroy the source asset. + for (cgltf_size i = 0, len = gltf->skins_count; i < len; ++i) { + importSkinningData(fasset->mSkins[i], gltf->skins[i]); + } + + // Compute surface orientation quaternions if necessary. + computeTangents(fasset); + + // Finally, load image files and create Filament Textures. + return createTextures(fasset); +} + +bool ResourceLoader::createTextures(details::FFilamentAsset* asset) const { + // Define a simple functor that creates a Filament Texture from a blob of texels. + // TODO: this could be optimized, e.g. do not generate mips if never mipmap-sampled, and use a + // more compact format when possible. + auto createTexture = [this, asset](stbi_uc* texels, uint32_t w, uint32_t h, bool srgb) { + Texture *tex = Texture::Builder() + .width(w) + .height(h) + .levels(0xff) + .format(srgb ? driver::TextureFormat::SRGB8_A8 : driver::TextureFormat::RGBA8) + .build(*mConfig.engine); + + Texture::PixelBufferDescriptor pbd(texels, + size_t(w * h * 4), + Texture::Format::RGBA, + Texture::Type::UBYTE, + (driver::BufferDescriptor::Callback) &free); + + tex->setImage(*mConfig.engine, 0, std::move(pbd)); + tex->generateMipmaps(*mConfig.engine); + asset->mTextures.push_back(tex); + return tex; + }; + + // Decode textures and associate them with material instance parameters. + // To prevent needless re-decoding, we create a small map of Filament Texture objects where the + // map key is (usually) a pointer to a URI string. In the case of buffer view textures, the + // cache key is a data pointer. + stbi_uc* texels; + int width, height, comp; + Texture* tex; + tsl::robin_map textures; + const TextureBinding* texbindings = asset->getTextureBindings(); + for (size_t i = 0, n = asset->getTextureBindingCount(); i < n; ++i) { + auto tb = texbindings[i]; + if (tb.data) { + tex = textures[tb.data]; + if (!tex) { + texels = stbi_load_from_memory((const stbi_uc*) *tb.data, tb.totalSize, + &width, &height, &comp, 4); + if (texels == nullptr) { + slog.e << "Unable to decode texture. " << io::endl; + return false; + } + textures[tb.data] = tex = createTexture(texels, width, height, tb.srgb); + } + } else { + tex = textures[(void*) tb.uri]; + if (!tex) { + utils::Path fullpath = this->mConfig.basePath + tb.uri; + texels = stbi_load(fullpath.c_str(), &width, &height, &comp, 4); + if (texels == nullptr) { + slog.e << "Unable to decode texture: " << tb.uri << io::endl; + return false; + } + textures[(void*) tb.uri] = tex = createTexture(texels, width, height, tb.srgb); + } + } + tb.materialInstance->setParameter(tb.materialParameter, tex, tb.sampler); + } + return true; +} + +void ResourceLoader::computeTangents(FFilamentAsset* asset) const { + // Declare vectors of normals and tangents, which we'll extract & convert from the source. + std::vector fp32Normals; + std::vector fp32Tangents; + std::vector fp32Positions; + std::vector fp32TexCoords; + std::vector ui32Triangles; + + auto computeQuats = [&](const cgltf_primitive& prim) { + + cgltf_size vertexCount = 0; + + // Collect accessors for normals, tangents, etc. + const int NUM_ATTRIBUTES = 8; + int slots[NUM_ATTRIBUTES] = {}; + const cgltf_accessor* accessors[NUM_ATTRIBUTES] = {}; + for (cgltf_size slot = 0; slot < prim.attributes_count; slot++) { + const cgltf_attribute& attr = prim.attributes[slot]; + // Ignore the second set of UV's. + if (attr.index != 0) { + continue; + } + vertexCount = attr.data->count; + slots[attr.type] = slot; + accessors[attr.type] = attr.data; + } + + // At a minimum we need normals to generate tangents. + auto normalsInfo = accessors[cgltf_attribute_type_normal]; + if (normalsInfo == nullptr || vertexCount == 0) { + return; + } + + short4* quats = (short4*) malloc(sizeof(short4) * vertexCount); + + geometry::SurfaceOrientation::Builder sob; + sob.vertexCount(vertexCount); + + // Convert normals into packed floats. + assert(normalsInfo->count == vertexCount); + assert(normalsInfo->type == cgltf_type_vec3); + fp32Normals.resize(vertexCount); + for (cgltf_size i = 0; i < vertexCount; ++i) { + cgltf_accessor_read_float(normalsInfo, i, &fp32Normals[i].x, 3); + } + sob.normals(fp32Normals.data()); + + // Convert tangents into packed floats. + auto tangentsInfo = accessors[cgltf_attribute_type_tangent]; + if (tangentsInfo) { + if (tangentsInfo->count != vertexCount || tangentsInfo->type != cgltf_type_vec4) { + slog.e << "Bad tangent count or type." << io::endl; + return; + } + fp32Tangents.resize(vertexCount); + for (cgltf_size i = 0; i < vertexCount; ++i) { + cgltf_accessor_read_float(tangentsInfo, i, &fp32Tangents[i].x, 4); + } + sob.tangents(fp32Tangents.data()); + } + + auto positionsInfo = accessors[cgltf_attribute_type_position]; + if (positionsInfo) { + if (positionsInfo->count != vertexCount || positionsInfo->type != cgltf_type_vec3) { + slog.e << "Bad position count or type." << io::endl; + return; + } + fp32Positions.resize(vertexCount); + for (cgltf_size i = 0; i < vertexCount; ++i) { + cgltf_accessor_read_float(positionsInfo, i, &fp32Positions[i].x, 3); + } + sob.positions(fp32Positions.data()); + } + + if (prim.indices) { + size_t triangleCount = prim.indices->count / 3; + ui32Triangles.resize(triangleCount); + cgltf_size j = 0; + for (auto& triangle : ui32Triangles) { + triangle.x = cgltf_accessor_read_index(prim.indices, j++); + triangle.y = cgltf_accessor_read_index(prim.indices, j++); + triangle.z = cgltf_accessor_read_index(prim.indices, j++); + } + } else { + size_t triangleCount = vertexCount / 3; + ui32Triangles.resize(triangleCount); + cgltf_size j = 0; + for (auto& triangle : ui32Triangles) { + triangle.x = j++; + triangle.y = j++; + triangle.z = j++; + } + } + + sob.triangleCount(ui32Triangles.size()); + sob.triangles(ui32Triangles.data()); + + auto texcoordsInfo = accessors[cgltf_attribute_type_texcoord]; + if (texcoordsInfo) { + if (texcoordsInfo->count != vertexCount || texcoordsInfo->type != cgltf_type_vec2) { + slog.e << "Bad texcoord count or type." << io::endl; + return; + } + fp32TexCoords.resize(vertexCount); + for (cgltf_size i = 0; i < vertexCount; ++i) { + cgltf_accessor_read_float(texcoordsInfo, i, &fp32TexCoords[i].x, 2); + } + sob.uvs(fp32TexCoords.data()); + } + + // Compute surface orientation quaternions. + auto helper = sob.build(); + helper.getQuats(quats, vertexCount); + + // Upload quaternions to the GPU. + auto callback = (VertexBuffer::BufferDescriptor::Callback) free; + VertexBuffer::BufferDescriptor bd(quats, vertexCount * sizeof(short4), callback); + VertexBuffer* vb = asset->mPrimMap.at(&prim); + vb->setBufferAt(*mConfig.engine, slots[cgltf_attribute_type_normal], std::move(bd)); + }; + + for (auto iter : asset->mNodeMap) { + const cgltf_mesh* mesh = iter.first->mesh; + if (mesh) { + cgltf_size nprims = mesh->primitives_count; + for (cgltf_size index = 0; index < nprims; ++index) { + computeQuats(mesh->primitives[index]); + } + } + } +} + +void ResourceLoader::normalizeWeights(details::FFilamentAsset* asset) const { + auto normalize = [](cgltf_accessor* data) { + if (data->type != cgltf_type_vec4 || data->component_type != cgltf_component_type_r_32f) { + slog.w << "Cannot normalize weights, unsupported attribute type." << io::endl; + return; + } + uint8_t* bytes = (uint8_t*) data->buffer_view->buffer->data; + float4* floats = (float4*) (bytes + data->offset + data->buffer_view->offset); + for (cgltf_size i = 0; i < data->count; ++i) { + float4 weights = floats[i]; + float sum = weights.x + weights.y + weights.z + weights.w; + floats[i] = weights / sum; + } + }; + const cgltf_data* gltf = asset->mSourceAsset; + cgltf_size mcount = gltf->meshes_count; + for (cgltf_size mindex = 0; mindex < mcount; ++mindex) { + const cgltf_mesh& mesh = gltf->meshes[mindex]; + cgltf_size pcount = mesh.primitives_count; + for (cgltf_size pindex = 0; pindex < pcount; ++pindex) { + const cgltf_primitive& prim = mesh.primitives[pindex]; + cgltf_size acount = prim.attributes_count; + for (cgltf_size aindex = 0; aindex < acount; ++aindex) { + const auto& attr = prim.attributes[aindex]; + if (attr.type == cgltf_attribute_type_weights) { + normalize(attr.data); + } + } + } + } +} + +} // namespace gltfio diff --git a/libs/gltfio/src/math.h b/libs/gltfio/src/math.h new file mode 100644 index 0000000000..35a5db5911 --- /dev/null +++ b/libs/gltfio/src/math.h @@ -0,0 +1,110 @@ +/* + * 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 GLTFIO_MATH_H +#define GLTFIO_MATH_H + +#include +#include +#include +#include +#include + +namespace gltfio { + +inline void decomposeMatrix(const filament::math::mat4f& mat, filament::math::float3* translation, + filament::math::quatf* rotation, filament::math::float3* scale) { + using namespace filament::math; + + // Extract translation. + *translation = mat[3].xyz; + + // Extract upper-left for determinant computation. + const float a = mat[0][0]; + const float b = mat[0][1]; + const float c = mat[0][2]; + const float d = mat[1][0]; + const float e = mat[1][1]; + const float f = mat[1][2]; + const float g = mat[2][0]; + const float h = mat[2][1]; + const float i = mat[2][2]; + const float A = e * i - f * h; + const float B = f * g - d * i; + const float C = d * h - e * g; + + // Extract scale. + const float det(a * A + b * B + c * C); + float scalex = length(float3({a, b, c})); + float scaley = length(float3({d, e, f})); + float scalez = length(float3({g, h, i})); + float3 s = { scalex, scaley, scalez }; + if (det < 0) { + s = -s; + } + *scale = s; + + // Remove scale from the matrix. + mat4f clone = mat; + clone[0] /= s.x; + clone[1] /= s.y; + clone[2] /= s.z; + + // Extract rotation. + *rotation = clone.toQuaternion(); +} + +inline filament::math::mat4f composeMatrix(const filament::math::float3& translation, + const filament::math::quatf& rotation, const filament::math::float3& scale) { + float tx = translation[0]; + float ty = translation[1]; + float tz = translation[2]; + float qx = rotation[0]; + float qy = rotation[1]; + float qz = rotation[2]; + float qw = rotation[3]; + float sx = scale[0]; + float sy = scale[1]; + float sz = scale[2]; + return filament::math::mat4f( + (1 - 2 * qy*qy - 2 * qz*qz) * sx, + (2 * qx*qy + 2 * qz*qw) * sy, + (2 * qx*qz - 2 * qy*qw) * sz, + 0.f, + (2 * qx*qy - 2 * qz*qw) * sx, + (1 - 2 * qx*qx - 2 * qz*qz) * sy, + (2 * qy*qz + 2 * qx*qw) * sz, + 0.f, + (2 * qx*qz + 2 * qy*qw) * sx, + (2 * qy*qz - 2 * qx*qw) * sy, + (1 - 2 * qx*qx - 2 * qy*qy) * sz, + 0.f, tx, ty, tz, 1.f); +} + +inline filament::math::mat3f matrixFromUvTransform(const float offset[2], float rotation, + const float scale[2]) { + float tx = offset[0]; + float ty = offset[1]; + float sx = scale[0]; + float sy = scale[1]; + float c = cos(rotation); + float s = sin(rotation); + return filament::math::mat3f(sx * c, sx * s, tx, -sy * s, sy * c, ty, 0, 0, 1); +}; + +} // namespace gltfio + +#endif // GLTFIO_MATH_H diff --git a/libs/gltfio/src/upcast.h b/libs/gltfio/src/upcast.h new file mode 100644 index 0000000000..dd454086d4 --- /dev/null +++ b/libs/gltfio/src/upcast.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 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_FILAMENT_UPCAST_H +#define TNT_FILAMENT_UPCAST_H + +/* + * Generates functions to safely upcast a pointer Bar* to FBar* + * FILAMENT_UPCAST() should be included in the header file + * declaring FBar, e.g.: + * + * #include + * + * class FBar : public Bar { + * }; + * + * FILAMENT_UPCAST(Bar) + * + */ + +#define FILAMENT_UPCAST(CLASS) \ + inline F##CLASS& upcast(CLASS& that) noexcept { \ + return static_cast(that); \ + } \ + inline const F##CLASS& upcast(const CLASS& that) noexcept { \ + return static_cast(that); \ + } \ + inline F##CLASS* upcast(CLASS* that) noexcept { \ + return static_cast(that); \ + } \ + inline F##CLASS const* upcast(CLASS const* that) noexcept { \ + return static_cast(that); \ + } + +#endif // TNT_FILAMENT_UPCAST_H