From 2cb0d57eed089e6dc004fb23c38ef39038e7607b Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Tue, 9 Aug 2022 09:52:07 +0800 Subject: [PATCH] separate IBL from skybox and add setBackgroundImage method --- android/CMakeLists.txt | 38 +- android/src/main/cpp/filament_android.cpp | 17 +- .../app/polyvox/filament/FilamentInterop.kt | 6 +- .../app/polyvox/filament/FilamentView.kt | 24 +- ios/Classes/FilamentMethodCallHandler.mm | 10 +- ios/include/imageio/BasisEncoder.h | 170 +++++++++ ios/include/imageio/HDRDecoder.h | 47 +++ ios/include/imageio/ImageDecoder.h | 69 ++++ ios/include/imageio/ImageDiffer.h | 34 ++ ios/include/imageio/ImageEncoder.h | 64 ++++ ios/src/FilamentViewer.cpp | 348 ++++++++++++++---- ios/src/FilamentViewer.hpp | 19 +- ios/src/HDRLoader.cpp | 91 +++++ ios/src/Utils.cpp | 160 ++++++++ ios/src/materials/image.filamat | Bin 0 -> 13322 bytes ios/src/materials/image.mat | 54 +++ 16 files changed, 1066 insertions(+), 85 deletions(-) create mode 100644 ios/include/imageio/BasisEncoder.h create mode 100644 ios/include/imageio/HDRDecoder.h create mode 100644 ios/include/imageio/ImageDecoder.h create mode 100644 ios/include/imageio/ImageDiffer.h create mode 100644 ios/include/imageio/ImageEncoder.h create mode 100644 ios/src/HDRLoader.cpp create mode 100644 ios/src/Utils.cpp create mode 100644 ios/src/materials/image.filamat create mode 100644 ios/src/materials/image.mat diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index ac2b7910..6b0cf8c4 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -14,7 +14,43 @@ add_library( src/main/cpp/StbProvider.cpp src/main/cpp/JobSystem.cpp ../ios/src/FilamentViewer.cpp + ../ios/src/streambuf.cpp + ../ios/src/imagematerial.c ) -target_link_libraries(filament_interop -landroid -llog -lfilament -lbackend -lfilameshio -lviewer -lfilamat -lgeometry -lutils -lfilabridge -lgltfio_core -lfilament-iblprefilter -limage -lcamutils -lfilaflat -ldracodec -libl -lktxreader -lstb -lEGL -lGLESv3 -lbluevk -lvkshaders -luberzlib -lsmol-v -luberarchive -lzstd) +target_link_libraries( + filament_interop + -landroid + -llog + -lfilament + -lbackend + -lfilameshio + -lviewer + -lfilamat + -lgeometry + -lutils + -lfilabridge + -lgltfio_core + -lfilament-iblprefilter + -limage + -lcamutils + -lfilaflat + -ldracodec + -libl + -lktxreader + -limageio + -lpng + -ltinyexr + -lz + -lstb + -lEGL + -lGLESv3 + -lbluevk + -lvkshaders + -luberzlib + -lsmol-v + -luberarchive + -lzstd + + ) diff --git a/android/src/main/cpp/filament_android.cpp b/android/src/main/cpp/filament_android.cpp index 553c076d..a1d60377 100644 --- a/android/src/main/cpp/filament_android.cpp +++ b/android/src/main/cpp/filament_android.cpp @@ -47,14 +47,27 @@ static void freeResource(ResourceBuffer rb) { extern "C" { - void load_skybox(void* viewer, const char* skyboxPath, const char* iblPath) { - ((FilamentViewer*)viewer)->loadSkybox(skyboxPath, iblPath); + void set_background_image(void* viewer, const char* path) { + ((FilamentViewer*)viewer)->setBackgroundImage(path); + } + + void load_skybox(void* viewer, const char* skyboxPath) { + ((FilamentViewer*)viewer)->loadSkybox(skyboxPath); + } + + void load_ibl(void* viewer, const char* iblPath) { + ((FilamentViewer*)viewer)->loadIbl(iblPath); } void remove_skybox(void* viewer) { ((FilamentViewer*)viewer)->removeSkybox(); } + + void remove_ibl(void* viewer) { + ((FilamentViewer*)viewer)->removeIbl(); + } + void load_glb(void* viewer, const char* assetPath) { ((FilamentViewer*)viewer)->loadGlb(assetPath); } diff --git a/android/src/main/kotlin/app/polyvox/filament/FilamentInterop.kt b/android/src/main/kotlin/app/polyvox/filament/FilamentInterop.kt index 39e5ac95..05a392f9 100644 --- a/android/src/main/kotlin/app/polyvox/filament/FilamentInterop.kt +++ b/android/src/main/kotlin/app/polyvox/filament/FilamentInterop.kt @@ -24,7 +24,9 @@ interface FilamentInterop : Library { am:AssetManager ) : Pointer; - fun load_skybox(viewer:Pointer, skyboxPath:String, iblPath:String) : Pointer; + fun load_skybox(viewer:Pointer, skyboxPath:String) : Pointer; + + fun load_ibl(viewer:Pointer, skyboxPath:String) : Pointer; fun load_glb(viewer:Pointer, uri:String) : Pointer; @@ -66,5 +68,7 @@ interface FilamentInterop : Library { fun remove_skybox(viewer:Pointer); + fun remove_ibl(viewer:Pointer); + fun set_background_image(viewer:Pointer, path:String); } diff --git a/android/src/main/kotlin/app/polyvox/filament/FilamentView.kt b/android/src/main/kotlin/app/polyvox/filament/FilamentView.kt index 9f2b83fe..903bda5a 100644 --- a/android/src/main/kotlin/app/polyvox/filament/FilamentView.kt +++ b/android/src/main/kotlin/app/polyvox/filament/FilamentView.kt @@ -94,6 +94,7 @@ PlatformView { private lateinit var assetManager : AssetManager + init { MethodChannel(binaryMessenger, PolyvoxFilamentPlugin.VIEW_TYPE + '_' + viewId).also { _methodChannel = it @@ -157,12 +158,29 @@ PlatformView { // val flutterJNI = FlutterJNI.Factory.provideFlutterJNI() // flutterJNI.updateJavaAssetManager(assetManager, flutterApplicationInfo.flutterAssetsDir) } - "loadSkybox" -> { - val args = call.arguments as ArrayList + "setBackgroundImage" -> { + val args = call.arguments as String val loader = FlutterInjector.instance().flutterLoader() - _lib.load_skybox(_viewer!!, loader.getLookupKeyForAsset(args[0] as String), loader.getLookupKeyForAsset(args[1] as String)) + _lib.set_background_image(_viewer!!, loader.getLookupKeyForAsset(args)) result.success("OK"); } + "loadSkybox" -> { + val args = call.arguments as String + val loader = FlutterInjector.instance().flutterLoader() + _lib.load_skybox(_viewer!!, loader.getLookupKeyForAsset(args)) + result.success("OK"); + } + "loadIbl" -> { + val args = call.arguments as String + val loader = FlutterInjector.instance().flutterLoader() + + _lib.load_ibl(_viewer!!, loader.getLookupKeyForAsset(args)) + result.success("OK"); + } + "removeIbl" -> { + _lib.remove_ibl(_viewer!!) + result.success(true); + } "removeSkybox" -> { _lib.remove_skybox(_viewer!!) result.success(true); diff --git a/ios/Classes/FilamentMethodCallHandler.mm b/ios/Classes/FilamentMethodCallHandler.mm index 8e5beaca..b31d6c20 100644 --- a/ios/Classes/FilamentMethodCallHandler.mm +++ b/ios/Classes/FilamentMethodCallHandler.mm @@ -39,11 +39,17 @@ using namespace std; return; } if([@"loadSkybox" isEqualToString:call.method]) { - _viewer->loadSkybox([call.arguments[0] UTF8String], [call.arguments[1] UTF8String]); + _viewer->loadSkybox([call.arguments UTF8String]); result(@"OK"); - } else if([@"loadSkybox" isEqualToString:call.method]) { + } else if([@"removeSkybox" isEqualToString:call.method]) { _viewer->removeSkybox(); result(@"OK"); + } else if([@"loadIbl" isEqualToString:call.method]) { + _viewer->loadIbl([call.arguments UTF8String]); + result(@"OK"); + } else if([@"removeIbl" isEqualToString:call.method]) { + _viewer->removeIbl(); + result(@"OK"); } else if([@"loadGlb" isEqualToString:call.method]) { _viewer->loadGlb([call.arguments UTF8String]); result(@"OK"); diff --git a/ios/include/imageio/BasisEncoder.h b/ios/include/imageio/BasisEncoder.h new file mode 100644 index 00000000..cc1f14c8 --- /dev/null +++ b/ios/include/imageio/BasisEncoder.h @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2022 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 IMAGE_BASISENCODER_H_ +#define IMAGE_BASISENCODER_H_ + +#include +#include +#include + +#include + +namespace image { + +struct BasisEncoderBuilderImpl; +struct BasisEncoderImpl; + +class UTILS_PUBLIC BasisEncoder { +public: + enum class IntermediateFormat { + UASTC, + ETC1S, + }; + + class Builder { + public: + /** + * Constructs a Ktx2 builder with a fixed number of miplevels and layers. + * + * The number of mips and layers is required up front to allow pre-allocation of the + * appropriate BasisU input vectors. + * + * @param mipCount number of mipmap levels, including the base; must be at least 1. + * @param layerCount either 1 or the number of layers in an array texture. + * + * For cubemaps and cubemap arrays, multiply the layer count by 6 and pack the faces in + * standard GL order. + */ + Builder(size_t mipCount, size_t layerCount) noexcept; + + ~Builder() noexcept; + Builder(Builder&& that) noexcept; + Builder& operator=(Builder&& that) noexcept; + + /** + * Enables the linear flag. (default value: FALSE) + * + * This does two things: + * (1) Specifies that the image should be encoded without a transfer function. + * (2) Adds a tag to the ktx file that tells the loader that no transfer function was used. + * + * Note that the tag does not actually affect the compression process, it's basically just a + * hint to the reader. At the time of this writing, BasisU does not make a distinction + * between sRGB targets and linear targets. + */ + Builder& linear(bool enabled) noexcept; + + /** + * Enables cubemap or cubemap array mode. (default value: FALSE) + * + * When this is enabled the number of layers should be divisible by 6. + */ + Builder& cubemap(bool enabled) noexcept; + + /** + * Chooses the intermediate format as described in the BasisU docs. (default value: UASTC) + * + * For highest quality, use UASTC. + */ + Builder& intermediateFormat(IntermediateFormat format) noexcept; + + /** + * Specifies that only the first component of the incoming LinearImage should be honored. + * + * default value: FALSE + */ + Builder& grayscale(bool enabled) noexcept; + + /** + * Specifies that the incoming image should be transfored from [-1, +1] to [0, 1] before it + * passed to the Basis encoder. + * + * default value: FALSE + */ + Builder& normals(bool enabled) noexcept; + + /** + * Initializes the basis encoder with the given number of jobs. + * + * default value: 4 + */ + Builder& jobs(size_t count) noexcept; + + /** + * Supresses status messages. + * + * default value: FALSE + */ + Builder& quiet(bool enabled) noexcept; + + /** + * Submits image data in linear floating-point format. + * + * This must be called for every miplevel. + */ + Builder& miplevel(size_t mipIndex, size_t layerIndex, const LinearImage& image) noexcept; + + /** + * Creates a BasisU encoder and returns null if an error occurred. + */ + BasisEncoder* build(); + + private: + BasisEncoderBuilderImpl* mImpl; + Builder(const Builder&) = delete; + Builder& operator=(const Builder&) = delete; + }; + + ~BasisEncoder() noexcept; + BasisEncoder(BasisEncoder&& that) noexcept; + BasisEncoder& operator=(BasisEncoder&& that) noexcept; + + /** + * Triggers compression of all miplevels and waits until all jobs are done. + * + * The resulting KTX2 contents can be retrieved using the getters below. + * + * @returns false if an error occurred. + */ + bool encode(); + + /** + * Gets the number of bytes in the generated KTX2 file. + * + * This can only be called if encode() is successfully called first. + */ + size_t getKtx2ByteCount() const noexcept; + + /** + * Gets the content of the generated KTX2 file. + * + * This memory is owned by BasisEncoder and is freed when the encoder is freed. + * This can only be called if encode() is successfully called first. + */ + uint8_t const* getKtx2Data() const noexcept; + +private: + BasisEncoder(BasisEncoderImpl*) noexcept; + BasisEncoder(const BasisEncoder&) = delete; + BasisEncoder& operator=(const BasisEncoder&) = delete; + BasisEncoderImpl* mImpl; + friend struct BasisEncoderBuilderImpl; +}; + +} // namespace image + +#endif // IMAGE_BASISENCODER_H_ diff --git a/ios/include/imageio/HDRDecoder.h b/ios/include/imageio/HDRDecoder.h new file mode 100644 index 00000000..bb7519ee --- /dev/null +++ b/ios/include/imageio/HDRDecoder.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 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 IMAGE_HDRDECODER_H_ +#define IMAGE_HDRDECODER_H_ + +#include + +namespace image { + +class HDRDecoder : public ImageDecoder::Decoder { +public: + static HDRDecoder* create(std::istream& stream); + static bool checkSignature(char const* buf); + + HDRDecoder(const HDRDecoder&) = delete; + HDRDecoder& operator=(const HDRDecoder&) = delete; + +private: + explicit HDRDecoder(std::istream& stream); + ~HDRDecoder() override; + + // ImageDecoder::Decoder interface + LinearImage decode() override; + + static const char sigRadiance[]; + static const char sigRGBE[]; + std::istream& mStream; + std::streampos mStreamStartPos; +}; + +} // namespace image + +#endif /* IMAGE_IMAGEDECODER_H_ */ diff --git a/ios/include/imageio/ImageDecoder.h b/ios/include/imageio/ImageDecoder.h new file mode 100644 index 00000000..cd341c49 --- /dev/null +++ b/ios/include/imageio/ImageDecoder.h @@ -0,0 +1,69 @@ +/* + * 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 IMAGE_IMAGEDECODER_H_ +#define IMAGE_IMAGEDECODER_H_ + +#include +#include + +#include + +#include + +namespace image { + +class UTILS_PUBLIC ImageDecoder { +public: + enum class ColorSpace { + LINEAR, + SRGB + }; + + // Returns linear floating-point data, or a non-valid image if an error occured. + static LinearImage decode(std::istream& stream, const std::string& sourceName, + ColorSpace sourceSpace = ColorSpace::SRGB); + + class Decoder { + public: + virtual LinearImage decode() = 0; + virtual ~Decoder() = default; + + ColorSpace getColorSpace() const noexcept { + return mColorSpace; + } + + void setColorSpace(ColorSpace colorSpace) noexcept { + mColorSpace = colorSpace; + } + + private: + ColorSpace mColorSpace = ColorSpace::SRGB; + }; + +private: + enum class Format { + NONE, + PNG, + HDR, + PSD, + EXR + }; +}; + +} // namespace image + +#endif /* IMAGE_IMAGEDECODER_H_ */ diff --git a/ios/include/imageio/ImageDiffer.h b/ios/include/imageio/ImageDiffer.h new file mode 100644 index 00000000..c3c752e3 --- /dev/null +++ b/ios/include/imageio/ImageDiffer.h @@ -0,0 +1,34 @@ +/* + * Copyright 2018 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 + +namespace image { + +enum class ComparisonMode { + SKIP, + COMPARE, + UPDATE, +}; + +// Saves an image to disk or does a load-and-compare, depending on comparison mode. +// This makes it easy for unit tests to have compare / update commands. +// The passed-in image is the "result image" and the expected image is the "golden image". +void updateOrCompare(LinearImage result, const utils::Path& golden, ComparisonMode, float epsilon); + +} // namespace image diff --git a/ios/include/imageio/ImageEncoder.h b/ios/include/imageio/ImageEncoder.h new file mode 100644 index 00000000..7cd4bd8f --- /dev/null +++ b/ios/include/imageio/ImageEncoder.h @@ -0,0 +1,64 @@ +/* + * 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 IMAGE_IMAGEENCODER_H_ +#define IMAGE_IMAGEENCODER_H_ + +#include +#include + +#include + +#include + +namespace image { + +class UTILS_PUBLIC ImageEncoder { +public: + enum class Format { + PNG, // 8-bit sRGB, 1 or 3 channels + PNG_LINEAR, // 8-bit linear RGB, 1 or 3 channels + HDR, // 8-bit linear RGBE, 3 channels only + RGBM, // 8-bit RGBM, as PNG, 3 channels only + PSD, // 16-bit sRGB or 32-bit linear RGB, 3 channels only + // Default: 16 bit + EXR, // 16-bit linear RGB (half-float), 3 channels only + // Default: PIZ compression + DDS, // 8-bit sRGB, 1, 2 or 3 channels; + // 16-bit or 32-bit linear RGB, 1, 2 or 3 channels + // Default: 16 bit + DDS_LINEAR, // 8-bit, 16-bit or 32-bit linear RGB, 1, 2 or 3 channels + // Default: 16 bit + RGB_10_11_11_REV, // RGBA PNG file, but containing 11_11_10 data + }; + + // Consumes linear floating-point data, returns false if unable to encode. + static bool encode(std::ostream& stream, Format format, const LinearImage& image, + const std::string& compression, const std::string& destName); + + static Format chooseFormat(const std::string& name, bool forceLinear = false); + static std::string chooseExtension(Format format); + + class Encoder { + public: + virtual bool encode(const LinearImage& image) = 0; + virtual ~Encoder() = default; + }; +}; + +} // namespace image + +#endif /* IMAGE_IMAGEENCODER_H_ */ diff --git a/ios/src/FilamentViewer.cpp b/ios/src/FilamentViewer.cpp index fc45198e..55da9fd2 100644 --- a/ios/src/FilamentViewer.cpp +++ b/ios/src/FilamentViewer.cpp @@ -16,6 +16,7 @@ #include "FilamentViewer.hpp" +#include "streambuf.hpp" #include #include @@ -46,6 +47,8 @@ #include +#include + #include "math.h" #include @@ -63,11 +66,14 @@ #include "Log.h" +#include "imagematerial.h" + using namespace filament; using namespace filament::math; using namespace gltfio; using namespace utils; using namespace std::chrono; +using namespace image; namespace filament { @@ -170,15 +176,161 @@ namespace polyvox // Always add a direct light source since it is required for shadowing. _sun = EntityManager::get().create(); - LightManager::Builder(LightManager::Type::DIRECTIONAL) + LightManager::Builder(LightManager::Type::SUN) .color(Color::cct(6500.0f)) - .intensity(100000.0f) - .direction(math::float3(0.0f, 1.0f, 0.0f)) + .intensity(150000.0f) + .direction(math::float3(0.0f, 0.0f, -1.0f)) .castShadows(false) // .castShadows(true) .build(*_engine, _sun); _scene->addEntity(_sun); + } + static constexpr float4 sFullScreenTriangleVertices[3] = { + {-1.0f, -1.0f, 1.0f, 1.0f}, + {3.0f, -1.0f, 1.0f, 1.0f}, + {-1.0f, 3.0f, 1.0f, 1.0f}}; + + static const uint16_t sFullScreenTriangleIndices[3] = {0, 1, 2}; + + void FilamentViewer::createImageRenderable() + { + + if (_imageEntity) + return; + + auto &em = EntityManager::get(); + _imageMaterial = Material::Builder() + .package(IMAGEMATERIAL_IMAGE_DATA, IMAGEMATERIAL_IMAGE_SIZE) + .build(*_engine); + + _imageVb = VertexBuffer::Builder() + .vertexCount(3) + .bufferCount(1) + .attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT4, 0) + .build(*_engine); + + _imageVb->setBufferAt( + *_engine, 0, {sFullScreenTriangleVertices, sizeof(sFullScreenTriangleVertices)}); + + _imageIb = IndexBuffer::Builder() + .indexCount(3) + .bufferType(IndexBuffer::IndexType::USHORT) + .build(*_engine); + + _imageIb->setBuffer(*_engine, + {sFullScreenTriangleIndices, sizeof(sFullScreenTriangleIndices)}); + + Entity imageEntity = em.create(); + RenderableManager::Builder(1) + .boundingBox({{}, {1.0f, 1.0f, 1.0f}}) + .material(0, _imageMaterial->getDefaultInstance()) + .geometry(0, RenderableManager::PrimitiveType::TRIANGLES, _imageVb, _imageIb, 0, 3) + .culling(false) + .build(*_engine, imageEntity); + + _scene->addEntity(imageEntity); + + _imageEntity = &imageEntity; + + Texture *texture = Texture::Builder() + .width(1) + .height(1) + .levels(1) + .format(Texture::InternalFormat::RGBA8) + .sampler(Texture::Sampler::SAMPLER_2D) + .build(*_engine); + static uint32_t pixel = 0; + Texture::PixelBufferDescriptor buffer(&pixel, 4, Texture::Format::RGBA, Texture::Type::UBYTE); + texture->setImage(*_engine, 0, std::move(buffer)); + } + + void FilamentViewer::setBackgroundImage(const char *resourcePath) + { + + if (colorGrading) + { + _engine->destroy(colorGrading); + } + ToneMapper *tm = new LinearToneMapper(); + colorGrading = ColorGrading::Builder() + .toneMapper(tm) + .build(*_engine); + delete tm; + + _view->setColorGrading(colorGrading); + + createImageRenderable(); + + if (_imageTexture) + { + _engine->destroy(_imageTexture); + _imageTexture = nullptr; + } + + ResourceBuffer bg = _loadResource(resourcePath); + + polyvox::streambuf sb((char *)bg.data, (char *)bg.data + bg.size); + + std::istream *inputStream = new std::istream(&sb); + + LinearImage *image = new LinearImage(ImageDecoder::decode( + *inputStream, resourcePath, ImageDecoder::ColorSpace::SRGB)); + + if (!image->isValid()) + { + Log("Invalid image : %s", resourcePath); + return; + } + + delete inputStream; + + _freeResource(bg); + + uint32_t channels = image->getChannels(); + uint32_t w = image->getWidth(); + uint32_t h = image->getHeight(); + _imageTexture = Texture::Builder() + .width(w) + .height(h) + .levels(0xff) + .format(channels == 3 ? Texture::InternalFormat::RGB16F : Texture::InternalFormat::RGBA16F) + .sampler(Texture::Sampler::SAMPLER_2D) + .build(*_engine); + + Texture::PixelBufferDescriptor::Callback freeCallback = [](void *buf, size_t, void *data) + { + delete reinterpret_cast(data); + }; + + Texture::PixelBufferDescriptor buffer( + image->getPixelRef(), + size_t(w * h * channels * sizeof(float)), + channels == 3 ? Texture::Format::RGB : Texture::Format::RGBA, + Texture::Type::FLOAT, + freeCallback); + + _imageTexture->setImage(*_engine, 0, std::move(buffer)); + // _imageTexture->generateMipmaps(*_engine); + + float srcWidth = _imageTexture->getWidth(); + float srcHeight = _imageTexture->getHeight(); + float dstWidth = _view->getViewport().width; + float dstHeight = _view->getViewport().height; + + mat3f transform( + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f); + + _imageMaterial->setDefaultParameter("transform", transform); + _imageMaterial->setDefaultParameter( + "image", _imageTexture, _imageSampler); + + _imageMaterial->setDefaultParameter("showImage", 1); + + _imageMaterial->setDefaultParameter( + "backgroundColor", RgbType::sRGB, float3(1.0f)); } FilamentViewer::~FilamentViewer() @@ -231,7 +383,7 @@ namespace polyvox for (size_t i = 0; i < resourceUriCount; i++) { string uri = relativeResourcePath + string(resourceUris[i]); - Log("Creating resource buffer for resource at %s",uri.c_str()); + Log("Creating resource buffer for resource at %s", uri.c_str()); ResourceBuffer buf = _loadResource(uri.c_str()); // using FunctionCallback = std::function; @@ -313,7 +465,7 @@ namespace polyvox _asset = nullptr; _animator = nullptr; - ResourceBuffer rbuf = _loadResource(uri); + ResourceBuffer rbuf = _loadResource(uri); // Parse the glTF file and create Filament entities. Log("Creating asset from JSON"); @@ -335,14 +487,16 @@ namespace polyvox // transformToUnitCube(); } - void FilamentViewer::removeAsset() { - if (!_asset) { + void FilamentViewer::removeAsset() + { + if (!_asset) + { Log("No asset loaded, ignoring call."); return; } mtx.lock(); - + _resourceLoader->evictResourceData(); _scene->removeEntities(_asset->getEntities(), _asset->getEntityCount()); _assetLoader->destroyAsset(_asset); @@ -354,11 +508,6 @@ namespace polyvox mtx.unlock(); } - void FilamentViewer::removeSkybox() { - _scene->setSkybox(nullptr); - } - - /// /// Sets the active camera to the GLTF camera node specified by [name]. /// N.B. Blender will generally export a three-node hierarchy - Camera1->Camera_Orientation->Camera2. @@ -368,23 +517,26 @@ namespace polyvox { Log("Attempting to set camera to %s.", cameraName); size_t count = _asset->getCameraEntityCount(); - if(count == 0) { - Log("Failed, no cameras found in current asset."); + if (count == 0) + { + Log("Failed, no cameras found in current asset."); return false; } - const utils::Entity* cameras = _asset->getCameraEntities(); + const utils::Entity *cameras = _asset->getCameraEntities(); Log("%zu cameras found in current asset", count); - for(int i=0; i < count; i++) { - - auto inst = _ncm->getInstance(cameras[i]); - const char* name = _ncm->getName(inst); - Log("Camera %d : %s", i, name); - if (strcmp(name, cameraName) == 0) { + for (int i = 0; i < count; i++) + { - Camera* camera = _engine->getCameraComponent(cameras[i]); + auto inst = _ncm->getInstance(cameras[i]); + const char *name = _ncm->getName(inst); + Log("Camera %d : %s", i, name); + if (strcmp(name, cameraName) == 0) + { + + Camera *camera = _engine->getCameraComponent(cameras[i]); _view->setCamera(camera); - + const Viewport &vp = _view->getViewport(); const double aspect = (double)vp.width / vp.height; @@ -400,7 +552,8 @@ namespace polyvox unique_ptr> FilamentViewer::getAnimationNames() { - if(!_asset) { + if (!_asset) + { Log("No asset, ignoring call."); return nullptr; } @@ -420,7 +573,8 @@ namespace polyvox unique_ptr> FilamentViewer::getTargetNames(const char *meshName) { - if(!_asset) { + if (!_asset) + { Log("No asset, ignoring call."); return nullptr; } @@ -432,12 +586,14 @@ namespace polyvox { Entity e = entities[i]; auto inst = _ncm->getInstance(e); - const char* name = _ncm->getName(inst); + const char *name = _ncm->getName(inst); Log("Got entity instance name %s", name); - if(strcmp(name, meshName) == 0) { + if (strcmp(name, meshName) == 0) + { size_t count = _asset->getMorphTargetCountAt(e); - for(int j=0; j< count; j++) { - const char* morphName = _asset->getMorphTargetNameAt(e, j); + for (int j = 0; j < count; j++) + { + const char *morphName = _asset->getMorphTargetNameAt(e, j); names->push_back(morphName); } break; @@ -446,39 +602,66 @@ namespace polyvox return names; } - void FilamentViewer::loadSkybox(const char *const skyboxPath, const char *const iblPath) + void FilamentViewer::loadSkybox(const char *const skyboxPath) { + if (!skyboxPath) + { + _scene->setSkybox(nullptr); + } + else + { + ResourceBuffer skyboxBuffer = _loadResource(skyboxPath); - ResourceBuffer skyboxBuffer = _loadResource(skyboxPath); + image::Ktx1Bundle *skyboxBundle = + new image::Ktx1Bundle(static_cast(skyboxBuffer.data), static_cast(skyboxBuffer.size)); + _skyboxTexture = ktxreader::Ktx1Reader::createTexture(_engine, skyboxBundle, false); + _skybox = filament::Skybox::Builder().environment(_skyboxTexture).build(*_engine); - image::Ktx1Bundle *skyboxBundle = - new image::Ktx1Bundle(static_cast(skyboxBuffer.data), static_cast(skyboxBuffer.size)); - _skyboxTexture = ktxreader::Ktx1Reader::createTexture(_engine, skyboxBundle, false); - _skybox = filament::Skybox::Builder().environment(_skyboxTexture).build(*_engine); + _scene->setSkybox(_skybox); + _freeResource(skyboxBuffer); + } + } - _scene->setSkybox(_skybox); - _freeResource(skyboxBuffer); + void FilamentViewer::removeSkybox() + { + _scene->setSkybox(nullptr); + } - Log("Loading IBL from %s", iblPath); + void FilamentViewer::removeIbl() + { + _scene->setIndirectLight(nullptr); + } - // Load IBL. - ResourceBuffer iblBuffer = _loadResource(iblPath); + void FilamentViewer::loadIbl(const char *const iblPath) + { + if (!iblPath) + { + _scene->setIndirectLight(nullptr); + } + else + { - image::Ktx1Bundle *iblBundle = new image::Ktx1Bundle( - static_cast(iblBuffer.data), static_cast(iblBuffer.size)); - math::float3 harmonics[9]; - iblBundle->getSphericalHarmonics(harmonics); - _iblTexture = ktxreader::Ktx1Reader::createTexture(_engine, iblBundle, false); - _indirectLight = IndirectLight::Builder() - .reflections(_iblTexture) - .irradiance(3, harmonics) - .intensity(30000.0f) - .build(*_engine); - _scene->setIndirectLight(_indirectLight); + Log("Loading IBL from %s", iblPath); - _freeResource(iblBuffer); + // Load IBL. + ResourceBuffer iblBuffer = _loadResource(iblPath); - Log("Skybox/IBL load complete."); + image::Ktx1Bundle *iblBundle = new image::Ktx1Bundle( + static_cast(iblBuffer.data), static_cast(iblBuffer.size)); + math::float3 harmonics[9]; + iblBundle->getSphericalHarmonics(harmonics); + _iblTexture = ktxreader::Ktx1Reader::createTexture(_engine, iblBundle, false); + _indirectLight = IndirectLight::Builder() + .reflections(_iblTexture) + .irradiance(3, harmonics) + .intensity(30000.0f) + .build(*_engine); + _scene->setIndirectLight(_indirectLight); + + _freeResource(iblBuffer); + + Log("Skybox/IBL load complete."); + } } void FilamentViewer::transformToUnitCube() @@ -514,13 +697,13 @@ namespace polyvox Log("Not ready for rendering"); return; } - + mtx.lock(); - if(_asset) { + if (_asset) + { updateMorphAnimation(); updateEmbeddedAnimation(); } - math::float3 eye, target, upward; manipulator->getLookAt(&eye, &target, &upward); @@ -532,7 +715,7 @@ namespace polyvox _renderer->render(_view); _renderer->endFrame(); } - mtx.unlock(); + mtx.unlock(); } void FilamentViewer::updateViewportAndCameraProjection(int width, int height, float contentScaleFactor) @@ -561,11 +744,13 @@ namespace polyvox void FilamentViewer::updateMorphAnimation() { - if(!_morphAnimationBuffer) { + if (!_morphAnimationBuffer) + { return; } - if (_morphAnimationBuffer->frameIndex == -1) { + if (_morphAnimationBuffer->frameIndex == -1) + { _morphAnimationBuffer->frameIndex++; _morphAnimationBuffer->startTime = high_resolution_clock::now(); applyWeights(_morphAnimationBuffer->frameData, _morphAnimationBuffer->numWeights); @@ -580,7 +765,9 @@ namespace polyvox duration dur = high_resolution_clock::now() - _morphAnimationBuffer->startTime; Log("Morph animation completed in %f ms (%d frames at framerate %f), final frame was %d", dur.count(), _morphAnimationBuffer->numFrames, 1000 / _morphAnimationBuffer->frameLengthInMs, _morphAnimationBuffer->frameIndex); _morphAnimationBuffer = nullptr; - } else if (frameIndex != _morphAnimationBuffer->frameIndex) { + } + else if (frameIndex != _morphAnimationBuffer->frameIndex) + { Log("Rendering frame %d (of a total %d)", frameIndex, _morphAnimationBuffer->numFrames); _morphAnimationBuffer->frameIndex = frameIndex; auto framePtrOffset = frameIndex * _morphAnimationBuffer->numWeights; @@ -589,43 +776,56 @@ namespace polyvox } } - void FilamentViewer::playAnimation(int index, bool loop) { - if(index > _animator->getAnimationCount() - 1) { + void FilamentViewer::playAnimation(int index, bool loop) + { + if (index > _animator->getAnimationCount() - 1) + { Log("Asset does not contain an animation at index %d", index); - } else { + } + else + { _embeddedAnimationBuffer = make_unique(index, _animator->getAnimationDuration(index), loop); } } - void FilamentViewer::stopAnimation() { + void FilamentViewer::stopAnimation() + { // TODO - does this need to be threadsafe? _embeddedAnimationBuffer = nullptr; } - void FilamentViewer::updateEmbeddedAnimation() { - if(!_embeddedAnimationBuffer) { + void FilamentViewer::updateEmbeddedAnimation() + { + if (!_embeddedAnimationBuffer) + { return; } duration dur = duration_cast>(high_resolution_clock::now() - _embeddedAnimationBuffer->lastTime); float startTime = 0; - if(!_embeddedAnimationBuffer->hasStarted) { + if (!_embeddedAnimationBuffer->hasStarted) + { _embeddedAnimationBuffer->hasStarted = true; _embeddedAnimationBuffer->lastTime = high_resolution_clock::now(); - } else if(dur.count() >= _embeddedAnimationBuffer->duration) { - if(_embeddedAnimationBuffer->loop) { + } + else if (dur.count() >= _embeddedAnimationBuffer->duration) + { + if (_embeddedAnimationBuffer->loop) + { _embeddedAnimationBuffer->lastTime = high_resolution_clock::now(); - } else { + } + else + { _embeddedAnimationBuffer = nullptr; return; } - } else { + } + else + { startTime = dur.count(); } _animator->applyAnimation(_embeddedAnimationBuffer->animationIndex, startTime); _animator->updateBoneMatrices(); - } } - diff --git a/ios/src/FilamentViewer.hpp b/ios/src/FilamentViewer.hpp index 7e3060ef..52959872 100644 --- a/ios/src/FilamentViewer.hpp +++ b/ios/src/FilamentViewer.hpp @@ -93,8 +93,11 @@ namespace polyvox { FilamentViewer(void* layer, LoadResource loadResource, FreeResource freeResource); ~FilamentViewer(); - void loadSkybox(const char* const skyboxUri, const char* const iblUri); + void loadSkybox(const char* const skyboxUri); void removeSkybox(); + + void loadIbl(const char* const iblUri); + void removeIbl(); void loadGlb(const char* const uri); void loadGltf(const char* const uri, const char* relativeResourcePath); @@ -137,8 +140,10 @@ namespace polyvox { Renderer* getRenderer(); + void setBackgroundImage(const char* resourcePath); + private: - + void createImageRenderable(); void loadResources(std::string relativeResourcePath); void transformToUnitCube(); void cleanup(); @@ -187,6 +192,16 @@ namespace polyvox { unique_ptr _morphAnimationBuffer; unique_ptr _embeddedAnimationBuffer; + Texture* _imageTexture = nullptr; + Entity* _imageEntity = nullptr; + VertexBuffer* _imageVb = nullptr; + IndexBuffer* _imageIb = nullptr; + Material* _imageMaterial = nullptr; + TextureSampler _imageSampler; + + ColorGrading *colorGrading = nullptr; + + }; diff --git a/ios/src/HDRLoader.cpp b/ios/src/HDRLoader.cpp new file mode 100644 index 00000000..eca50e76 --- /dev/null +++ b/ios/src/HDRLoader.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include + +#include + +#include + +#include "common/NioUtils.h" + +using namespace filament; +using namespace image; +using namespace utils; + +using PixelBufferDescriptor = Texture::PixelBufferDescriptor; + +jlong nCreateHDRTexture(JNIEnv* env, jclass, + jlong nativeEngine, jobject javaBuffer, jint remaining, jint internalFormat) { + slog.e << "Creating HDR texture." << io::endl; + Engine* engine = (Engine*) nativeEngine; + AutoBuffer buffer(env, javaBuffer, remaining); + Texture::InternalFormat textureFormat = (Texture::InternalFormat) internalFormat; + + auto dataPtr = (char const*) buffer.getData(); + const size_t byteCount = buffer.getSize(); + + // This creates a copy but it's the easest way to create a memory stream. + std::string ins(dataPtr, byteCount); + std::istringstream in(ins); + + LinearImage* image = new LinearImage(ImageDecoder::decode(in, "memory.hdr")); + + // This can happen if a decoding error occurs. + if (image->getChannels() != 3) { + delete image; + return 0; + } + + Texture* texture = Texture::Builder() + .width(image->getWidth()) + .height(image->getHeight()) + .levels(0xff) + .sampler(Texture::Sampler::SAMPLER_2D) + .format(textureFormat) + .build(*engine); + + if (texture == nullptr) { + slog.e << "Unable to create Filament Texture from HDR image." << io::endl; + delete image; + return 0; + } + + PixelBufferDescriptor::Callback freeCallback = [](void* buf, size_t, void* userdata) { + delete (LinearImage*) userdata; + }; + + PixelBufferDescriptor pbd( + (void const* ) image->getPixelRef(), + image->getWidth() * image->getHeight() * 3 * sizeof(float), + PixelBufferDescriptor::PixelDataFormat::RGB, + PixelBufferDescriptor::PixelDataType::FLOAT, + freeCallback, + image); + + // Note that the setImage call could fail (e.g. due to an invalid combination of internal format + // and PixelDataFormat) but there is no way of detecting such a failure. + texture->setImage(*engine, 0, std::move(pbd)); + + texture->generateMipmaps(*engine); + + return (jlong) texture; +} diff --git a/ios/src/Utils.cpp b/ios/src/Utils.cpp new file mode 100644 index 00000000..ce504600 --- /dev/null +++ b/ios/src/Utils.cpp @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include + +#include +#include + +#include "common/NioUtils.h" + +#include + +using namespace filament; +using namespace filament::math; +using namespace image; + +jlong nCreateHDRTexture(JNIEnv* env, jclass, + jlong nativeEngine, jobject javaBuffer, jint remaining, jint internalFormat); + +static jlong nCreateKTXTexture(JNIEnv* env, jclass, + jlong nativeEngine, jobject javaBuffer, jint remaining, jboolean srgb) { + Engine* engine = (Engine*) nativeEngine; + AutoBuffer buffer(env, javaBuffer, remaining); + KtxBundle* bundle = new KtxBundle((const uint8_t*) buffer.getData(), buffer.getSize()); + return (jlong) ktx::createTexture(engine, *bundle, srgb, [](void* userdata) { + KtxBundle* bundle = (KtxBundle*) userdata; + delete bundle; + }, bundle); +} + +static jlong nCreateIndirectLight(JNIEnv* env, jclass, + jlong nativeEngine, jobject javaBuffer, jint remaining, jboolean srgb) { + Engine* engine = (Engine*) nativeEngine; + AutoBuffer buffer(env, javaBuffer, remaining); + KtxBundle* bundle = new KtxBundle((const uint8_t*) buffer.getData(), buffer.getSize()); + Texture* cubemap = ktx::createTexture(engine, *bundle, srgb, [](void* userdata) { + KtxBundle* bundle = (KtxBundle*) userdata; + delete bundle; + }, bundle); + + float3 harmonics[9]; + bundle->getSphericalHarmonics(harmonics); + + IndirectLight* indirectLight = IndirectLight::Builder() + .reflections(cubemap) + .irradiance(3, harmonics) + .intensity(30000) + .build(*engine); + + return (jlong) indirectLight; +} + +static jlong nCreateSkybox(JNIEnv* env, jclass, + jlong nativeEngine, jobject javaBuffer, jint remaining, jboolean srgb, jobject assetManager, jstring outpath) { + + AAssetManager *mgr = AAssetManager_fromJava(env, assetManager); + + AAsset *asset = AAssetManager_open(mgr, "envs/default_env/default_env_skybox.ktx", AASSET_MODE_BUFFER); + if(asset == nullptr) { + __android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Couldn't open asset"); + return 0; + } + + off_t length = AAsset_getLength(asset); + const void * buffer = AAsset_getBuffer(asset); + jboolean isCopy = (jboolean)false; + const char* out_cstr = env->GetStringUTFChars(outpath, &isCopy); + + __android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Opening outfile %s for writing", out_cstr); + + FILE* outfile = fopen(out_cstr, "w"); + + fwrite(buffer, 1, length, outfile); + + fclose(outfile); + + __android_log_print(ANDROID_LOG_VERBOSE, "filament_api", "Closed outfile %s", out_cstr); + + Engine* engine = (Engine*) nativeEngine; + // __android_log_print(ANDROID_LOG_VERBOSE, "UTILS", "CREATing autobuffer"); + // AutoBuffer buffer(env, javaBuffer, remaining); + // __android_log_print(ANDROID_LOG_VERBOSE, "UTILS", "CREATied autobuffer"); + + KtxBundle* bundle = new KtxBundle((const uint8_t*) buffer, length); + // KtxBundle* bundle = new KtxBundle((const uint8_t*) buffer.getData(), buffer.getSize()); + __android_log_print(ANDROID_LOG_VERBOSE, "UTILS", "CREATED BUNDLE FROM API"); + + + Texture* cubemap = ktx::createTexture(engine, *bundle, srgb, [](void* userdata) { + KtxBundle* bundle = (KtxBundle*) userdata; + delete bundle; + }, bundle); + __android_log_print(ANDROID_LOG_VERBOSE, "UTILS", "CREATED TEXTURE"); + return (jlong) Skybox::Builder().environment(cubemap).showSun(true).build(*engine); +} + +static jboolean nGetSphericalHarmonics(JNIEnv* env, jclass, jobject javaBuffer, jint remaining, + jfloatArray outSphericalHarmonics_) { + AutoBuffer buffer(env, javaBuffer, remaining); + KtxBundle bundle((const uint8_t*) buffer.getData(), buffer.getSize()); + + jfloat* outSphericalHarmonics = env->GetFloatArrayElements(outSphericalHarmonics_, nullptr); + const auto success = bundle.getSphericalHarmonics( + reinterpret_cast(outSphericalHarmonics) + ); + env->ReleaseFloatArrayElements(outSphericalHarmonics_, outSphericalHarmonics, JNI_ABORT); + + return success ? JNI_TRUE : JNI_FALSE; +} + +JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return -1; + } + + int rc; + + // KTXLoader + jclass ktxloaderClass = env->FindClass("app/polyvox/filament/KTXLoader2"); + if (ktxloaderClass == nullptr) return JNI_ERR; + static const JNINativeMethod ktxMethods[] = { + {(char*)"nCreateKTXTexture", (char*)"(JLjava/nio/Buffer;IZ)J", reinterpret_cast(nCreateKTXTexture)}, + {(char*)"nCreateIndirectLight", (char*)"(JLjava/nio/Buffer;IZ)J", reinterpret_cast(nCreateIndirectLight)}, + {(char*)"nCreateSkybox", (char*)"(JLjava/nio/Buffer;IZLandroid/content/res/AssetManager;Ljava/lang/String;)J", reinterpret_cast(nCreateSkybox)}, + {(char*)"nGetSphericalHarmonics", (char*)"(Ljava/nio/Buffer;I[F)Z", reinterpret_cast(nGetSphericalHarmonics)}, + }; + rc = env->RegisterNatives(ktxloaderClass, ktxMethods, sizeof(ktxMethods) / sizeof(JNINativeMethod)); + if (rc != JNI_OK) return rc; + + // HDRLoader + jclass hdrloaderClass = env->FindClass("com/google/android/filament/utils/HDRLoader"); + if (hdrloaderClass == nullptr) return JNI_ERR; + static const JNINativeMethod hdrMethods[] = { + {(char*)"nCreateHDRTexture", (char*)"(JLjava/nio/Buffer;II)J", reinterpret_cast(nCreateHDRTexture)}, + }; + rc = env->RegisterNatives(hdrloaderClass, hdrMethods, sizeof(hdrMethods) / sizeof(JNINativeMethod)); + if (rc != JNI_OK) return rc; + + return JNI_VERSION_1_6; +} diff --git a/ios/src/materials/image.filamat b/ios/src/materials/image.filamat new file mode 100644 index 0000000000000000000000000000000000000000..36f02b17582ea8c510377de35b052cff0a86d9c0 GIT binary patch literal 13322 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 aVC6+d*Sp|*e8S(O-mBgRq^a;3 literal 0 HcmV?d00001 diff --git a/ios/src/materials/image.mat b/ios/src/materials/image.mat new file mode 100644 index 00000000..7e285fd8 --- /dev/null +++ b/ios/src/materials/image.mat @@ -0,0 +1,54 @@ +material { + name : Image, + parameters : [ + { + type : sampler2d, + name : image + }, + { + type : mat3, + name : transform, + precision : high + }, + { + type : float3, + name : backgroundColor + }, + { + type : int, + name : showImage + } + ], + variables : [ + imageUV + ], + vertexDomain : device, + depthWrite : false, + shadingModel : unlit, + variantFilter : [ skinning, shadowReceiver, vsm ], + culling: none +} + +vertex { + void materialVertex(inout MaterialVertexInputs material) { + material.imageUV.st = getPosition().st * 0.5 + 0.5; + } +} + +fragment { + void material(inout MaterialInputs material) { + prepareMaterial(material); + + vec4 bg = vec4(materialParams.backgroundColor, 1.0); + highp vec2 uv = (materialParams.transform * vec3(saturate(variable_imageUV.st), 1.0)).st; + if (materialParams.showImage == 0 || uv.s > 1.0 || uv.s < 0.0 || uv.t < 0.0 || uv.t > 1.0) { + material.baseColor = bg; + } else { + uv.t = 1.0 - uv.t; + vec4 color = max(texture(materialParams_image, uv.st), 0.0); + color.rgb *= color.a; + // Manual, pre-multiplied srcOver with opaque destination optimization + material.baseColor.rgb = color.rgb + bg.rgb * (1.0 - color.a); + } + } +}