On certain linux, macOS environment, there is already a system getopt. This often creates conflict when compiling filament. Here we alias utils::getopt to either the system getopt (if present) or third_party/getopt. Fixes #7551
583 lines
21 KiB
C++
583 lines
21 KiB
C++
/*
|
|
* Copyright (C) 2026 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 "common/arguments.h"
|
|
#include "filament/TransformManager.h"
|
|
|
|
#include <filament/Camera.h>
|
|
#include <filament/Engine.h>
|
|
#include <filament/IndexBuffer.h>
|
|
#include <filament/Material.h>
|
|
#include <filament/MaterialInstance.h>
|
|
#include <filament/RenderableManager.h>
|
|
#include <filament/Scene.h>
|
|
#include <filament/Skybox.h>
|
|
#include <filament/Texture.h>
|
|
#include <filament/TextureSampler.h>
|
|
#include <filament/VertexBuffer.h>
|
|
#include <filament/View.h>
|
|
|
|
#include <utils/EntityManager.h>
|
|
|
|
#include <utils/Path.h>
|
|
|
|
#include <filamentapp/Config.h>
|
|
#include <filamentapp/FilamentApp.h>
|
|
|
|
#include <utils/getopt.h>
|
|
|
|
#include <stb_image.h>
|
|
|
|
#include <iostream> // for cerr
|
|
#include <memory>
|
|
#include <string> // for printing usage/help
|
|
|
|
#include "generated/resources/resources.h"
|
|
|
|
using namespace filament;
|
|
using utils::Entity;
|
|
using utils::EntityManager;
|
|
using utils::Path;
|
|
using MinFilter = TextureSampler::MinFilter;
|
|
using MagFilter = TextureSampler::MagFilter;
|
|
|
|
struct Vertex {
|
|
filament::math::float2 position;
|
|
filament::math::float2 uv;
|
|
};
|
|
|
|
static const Vertex QUAD_VERTICES[4] = {
|
|
{{-1, -1}, {0, 0}},
|
|
{{ 1, -1}, {1, 0}},
|
|
{{-1, 1}, {0, 1}},
|
|
{{ 1, 1}, {1, 1}},
|
|
};
|
|
|
|
static constexpr uint16_t QUAD_INDICES[6] = {
|
|
0, 1, 2,
|
|
3, 2, 1,
|
|
};
|
|
|
|
static void printUsage(char* name) {
|
|
std::string exec_name(utils::Path(name).getName());
|
|
std::string usage("HELLOASYNC creates resources asynchronously\n"
|
|
"Usage:\n"
|
|
" HELLOASYNC [options]\n"
|
|
"Options:\n"
|
|
" --help, -h\n"
|
|
" Prints this message\n\n"
|
|
"API_USAGE");
|
|
const std::string from("HELLOASYNC");
|
|
for (size_t pos = usage.find(from); pos != std::string::npos; pos = usage.find(from, pos)) {
|
|
usage.replace(pos, from.length(), exec_name);
|
|
}
|
|
const std::string apiUsage("API_USAGE");
|
|
for (size_t pos = usage.find(apiUsage); pos != std::string::npos;
|
|
pos = usage.find(apiUsage, pos)) {
|
|
usage.replace(pos, apiUsage.length(), samples::getBackendAPIArgumentsUsage());
|
|
}
|
|
std::cout << usage;
|
|
}
|
|
|
|
static int handleCommandLineArguments(int argc, char* argv[], Config& config) {
|
|
static constexpr const char* OPTSTR = "ha:";
|
|
static const utils::getopt::option OPTIONS[] = {
|
|
{ "help", utils::getopt::no_argument, nullptr, 'h' },
|
|
{ "api", utils::getopt::required_argument, nullptr, 'a' },
|
|
{ nullptr, 0, nullptr, 0 }
|
|
};
|
|
int opt;
|
|
int option_index = 0;
|
|
while ((opt = utils::getopt::getopt_long(argc, argv, OPTSTR, OPTIONS, &option_index)) >= 0) {
|
|
std::string arg(utils::getopt::optarg ? utils::getopt::optarg : "");
|
|
switch (opt) {
|
|
default:
|
|
case 'h':
|
|
printUsage(argv[0]);
|
|
exit(0);
|
|
case 'a':
|
|
config.backend = samples::parseArgumentsForBackend(arg);
|
|
break;
|
|
}
|
|
}
|
|
return utils::getopt::optind;
|
|
}
|
|
|
|
struct App {
|
|
// Global data
|
|
Engine* engine = nullptr;
|
|
Entity camera;
|
|
Scene* scene;
|
|
Skybox* skybox = nullptr;
|
|
Camera* cam = nullptr;
|
|
Material* mat = nullptr;
|
|
|
|
|
|
// --------------------------------------------------------------------------------------------
|
|
// Everything below this point is for demonstrating async logic.
|
|
|
|
// The number of objects to be created.
|
|
static constexpr int OBJECT_COUNT = 400;
|
|
static constexpr int OBJECT_COUNT_PER_ROW = 20;
|
|
static constexpr int ROW_COUNT =
|
|
(OBJECT_COUNT + OBJECT_COUNT_PER_ROW - 1) / OBJECT_COUNT_PER_ROW;
|
|
|
|
// For demonstration purposes, we load one image and it is shared for every ObjectData instance.
|
|
int imageWidth;
|
|
int imageHeight;
|
|
int imageChannels;
|
|
|
|
struct StbImageDeleter {
|
|
void operator()(stbi_uc* p) const {
|
|
// We delay freeing the stb image until after the engine has completely shut down. This
|
|
// ensures the data remains valid while the engine flushing pending tasks (see
|
|
// `updateTexture`).
|
|
// Note: This cleanup is specific to this sample because the image is shared across
|
|
// multiple objects. In a standard application, memory should be released via the
|
|
// cleanup callback in `PixelBufferDescriptor`. (see `updateTexture`)
|
|
stbi_image_free(p);
|
|
}
|
|
};
|
|
std::unique_ptr<stbi_uc, StbImageDeleter> imageData;
|
|
|
|
// Object data associated with a single renderable object.
|
|
struct ObjectData {
|
|
Texture* tex = nullptr;
|
|
MaterialInstance* matInstance = nullptr;
|
|
VertexBuffer* vb = nullptr;
|
|
IndexBuffer* ib = nullptr;
|
|
Entity renderable;
|
|
filament::math::mat4f baseTransform;
|
|
|
|
bool texReady = false;
|
|
bool vbReady = false;
|
|
bool ibReady = false;
|
|
[[nodiscard]] bool isReadyToCreateRenderable() const {
|
|
return texReady && vbReady && ibReady;
|
|
}
|
|
} objectData[OBJECT_COUNT];
|
|
|
|
// The number of objects currently being loaded.
|
|
int loadingObjectIndex = 0;
|
|
|
|
// To prevent calling APIs during shutdown. This variable is always referenced in the main(app)
|
|
// thread, so synchronization is unnecessary.
|
|
bool shuttingDown = false;
|
|
|
|
// Completion callbacks for chained actions. We store them here instead of directly passing them
|
|
// to async APIs as parameters for better maintainability and legibility.
|
|
using OnLoadImageComplete = Engine::AsyncCompletionCallback;
|
|
using OnCreateTextureComplete = Texture::AsyncCompletionCallback;
|
|
using OnTextureUpdateComplete = Texture::AsyncCompletionCallback;
|
|
using OnCreateVertexBufferComplete = VertexBuffer::AsyncCompletionCallback;
|
|
using OnVertexBufferUpdateComplete = VertexBuffer::AsyncCompletionCallback;
|
|
using OnCreateIndexBufferComplete = IndexBuffer::AsyncCompletionCallback;
|
|
using OnIndexBufferUpdateComplete = IndexBuffer::AsyncCompletionCallback;
|
|
OnLoadImageComplete onLoadImageComplete; // -> Create material & start async renderable creation
|
|
OnCreateTextureComplete onCreateTextureComplete; // -> Update texture
|
|
OnTextureUpdateComplete onTextureUpdateComplete; // -> Create mat instance & mark texture ready!
|
|
OnCreateVertexBufferComplete onCreateVertexBufferComplete; // -> Update vertex buffer
|
|
OnVertexBufferUpdateComplete onVertexBufferUpdateComplete; // -> Mark vertex buffer ready!
|
|
OnCreateIndexBufferComplete onCreateIndexBufferComplete; // -> Update index buffer
|
|
OnIndexBufferUpdateComplete onIndexBufferUpdateComplete; // -> Mark index buffer ready!
|
|
|
|
// These methods below handle resource creation and updates. They are intended to support for
|
|
// both standard synchronous flows and asynchronous operations. Note that they must be invoked
|
|
// from the main thread as they call "Filament APIs" in it. In this sample, you see some methods
|
|
// are called directly inside the asynchronous completion callbacks, which is safe because the
|
|
// callbacks are guaranteed to run on the main thread.
|
|
|
|
void createMaterial() {
|
|
mat = Material::Builder()
|
|
.package(RESOURCES_BAKEDTEXTURE_DATA, RESOURCES_BAKEDTEXTURE_SIZE)
|
|
.build(*engine);
|
|
}
|
|
|
|
void startLoadingOneRenderable() {
|
|
if (loadingObjectIndex >= OBJECT_COUNT) {
|
|
return;
|
|
}
|
|
|
|
// `loadingObjectIndex` doesn't have to be an atomic variable because this method is always
|
|
// called from the main thread.
|
|
int index = loadingObjectIndex++;
|
|
auto* data = &objectData[index];
|
|
|
|
// Create required resources for a renderable in parallel
|
|
createTexture(data, onCreateTextureComplete);
|
|
createVertexBuffer(data, onCreateVertexBufferComplete);
|
|
createIndexBuffer(data, onCreateIndexBufferComplete);
|
|
}
|
|
|
|
void loadImage(OnLoadImageComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
utils::Invocable<void()> command = [this](){
|
|
Path const path =
|
|
FilamentApp::getRootAssetsPath() + "textures/Moss_01/Moss_01_Color.png";
|
|
if (!path.exists()) {
|
|
std::cerr << "The texture " << path << " does not exist" << std::endl;
|
|
exit(1);
|
|
}
|
|
imageData.reset(stbi_load(path.c_str(), &imageWidth, &imageHeight,
|
|
&imageChannels, 4));
|
|
if (!imageData) {
|
|
std::cerr << "The texture " << path << " could not be loaded" << std::endl;
|
|
exit(1);
|
|
}
|
|
};
|
|
|
|
if (callback) {
|
|
engine->runCommandAsync(std::move(command), nullptr, std::move(callback));
|
|
} else {
|
|
command();
|
|
}
|
|
}
|
|
|
|
void createTexture(void* user, OnCreateTextureComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
auto builder = Texture::Builder()
|
|
.width(static_cast<uint32_t>(imageWidth))
|
|
.height(static_cast<uint32_t>(imageHeight))
|
|
.levels(1)
|
|
// (For testing purposes) This will add a chained asynchronous operation during the
|
|
// texture creation.
|
|
.swizzle(Texture::Swizzle::SUBSTITUTE_ZERO, Texture::Swizzle::CHANNEL_1,
|
|
Texture::Swizzle::SUBSTITUTE_ZERO, Texture::Swizzle::SUBSTITUTE_ZERO)
|
|
.sampler(Texture::Sampler::SAMPLER_2D)
|
|
.format(Texture::InternalFormat::RGBA8);
|
|
if (callback) {
|
|
builder.async(nullptr, std::move(callback), user);
|
|
}
|
|
data->tex = builder.build(*engine);
|
|
}
|
|
|
|
void updateTexture(void* user, OnTextureUpdateComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
Texture::PixelBufferDescriptor buffer(imageData.get(),
|
|
static_cast<size_t>(imageWidth * imageHeight * 4),
|
|
Texture::Format::RGBA, Texture::Type::UBYTE
|
|
// Don't destroy the loaded image since it needs to be reused.
|
|
/*, (Texture::PixelBufferDescriptor::Callback)&stbi_image_free*/);
|
|
if (callback) {
|
|
data->tex->setImageAsync(*engine, 0, std::move(buffer),
|
|
nullptr, std::move(callback), user);
|
|
} else {
|
|
data->tex->setImage(*engine, 0, std::move(buffer));
|
|
}
|
|
}
|
|
|
|
void createMaterialInstance(void* user) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
data->matInstance = mat->createInstance();
|
|
TextureSampler sampler(MinFilter::LINEAR, MagFilter::LINEAR);
|
|
data->matInstance->setParameter("albedo", data->tex, sampler);
|
|
}
|
|
|
|
void textureReady(void* user) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
data->texReady = true;
|
|
// try creating renderable
|
|
mayCreateRenderable(user);
|
|
}
|
|
|
|
void createVertexBuffer(void* user, OnCreateVertexBufferComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
static_assert(sizeof(Vertex) == 16, "Strange vertex size.");
|
|
auto builder = VertexBuffer::Builder()
|
|
.vertexCount(4)
|
|
.bufferCount(1)
|
|
.attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT2, 0, 16)
|
|
.attribute(VertexAttribute::UV0, 0, VertexBuffer::AttributeType::FLOAT2, 8, 16);
|
|
if (callback) {
|
|
builder.async(nullptr, std::move(callback), user);
|
|
}
|
|
data->vb = builder.build(*engine);
|
|
}
|
|
|
|
void updateVertexBuffer(void* user, OnVertexBufferUpdateComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
if (callback) {
|
|
data->vb->setBufferAtAsync(*engine, 0,
|
|
VertexBuffer::BufferDescriptor(QUAD_VERTICES, 64, nullptr), 0,
|
|
nullptr, std::move(callback), user);
|
|
} else {
|
|
data->vb->setBufferAt(*engine, 0,
|
|
VertexBuffer::BufferDescriptor(QUAD_VERTICES, 64, nullptr));
|
|
}
|
|
}
|
|
|
|
void vertexBufferReady(void* user) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
data->vbReady = true;
|
|
// try creating renderable
|
|
mayCreateRenderable(user);
|
|
}
|
|
|
|
void createIndexBuffer(void* user, OnCreateIndexBufferComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
auto builder = IndexBuffer::Builder()
|
|
.indexCount(6)
|
|
.bufferType(IndexBuffer::IndexType::USHORT);
|
|
if (callback) {
|
|
builder.async(nullptr, std::move(callback), user);
|
|
}
|
|
data->ib = builder.build(*engine);
|
|
}
|
|
|
|
void updateIndexBuffer(void* user, OnIndexBufferUpdateComplete callback = nullptr) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
if (callback) {
|
|
data->ib->setBufferAsync(*engine,
|
|
IndexBuffer::BufferDescriptor(QUAD_INDICES, 12, nullptr), 0,
|
|
nullptr, std::move(callback), user);
|
|
} else {
|
|
data->ib->setBuffer(*engine,
|
|
IndexBuffer::BufferDescriptor(QUAD_INDICES, 12, nullptr));
|
|
}
|
|
}
|
|
|
|
void indexBufferReady(void* user) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
data->ibReady = true;
|
|
// try creating renderable
|
|
mayCreateRenderable(user);
|
|
}
|
|
|
|
void mayCreateRenderable(void* user) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
if (data->isReadyToCreateRenderable()) {
|
|
createRenderable(user);
|
|
// Done with loading a renderable, load the next one.
|
|
startLoadingOneRenderable();
|
|
}
|
|
}
|
|
|
|
void createRenderable(void* user) {
|
|
if (shuttingDown) {
|
|
return;
|
|
}
|
|
|
|
auto* data = static_cast<ObjectData*>(user);
|
|
data->renderable = EntityManager::get().create();
|
|
RenderableManager::Builder(1)
|
|
.boundingBox({{ -1, -1, -1 }, { 1, 1, 1 }})
|
|
.material(0, data->matInstance)
|
|
.geometry(0, RenderableManager::PrimitiveType::TRIANGLES, data->vb, data->ib, 0, 6)
|
|
.culling(false)
|
|
.receiveShadows(false)
|
|
.castShadows(false)
|
|
.build(*engine, data->renderable);
|
|
scene->addEntity(data->renderable);
|
|
}
|
|
};
|
|
|
|
int main(int argc, char** argv) {
|
|
Config config;
|
|
config.title = "helloasync";
|
|
config.asynchronousMode = backend::AsynchronousMode::THREAD_PREFERRED;
|
|
handleCommandLineArguments(argc, argv, config);
|
|
|
|
App app;
|
|
|
|
auto setup = [&app](Engine* engine, View* view, Scene* scene) {
|
|
app.engine = engine;
|
|
app.scene = scene;
|
|
|
|
// Set up view (Skybox & Camera)
|
|
app.skybox = Skybox::Builder().color({0.1, 0.125, 0.25, 1.0}).build(*engine);
|
|
scene->setSkybox(app.skybox);
|
|
|
|
app.camera = EntityManager::get().create();
|
|
app.cam = engine->createCamera(app.camera);
|
|
const float zoom = 12.0;
|
|
const float aspect =
|
|
static_cast<float>(view->getViewport().width) / view->getViewport().height;
|
|
app.cam->setProjection(Camera::Projection::ORTHO, -zoom, zoom,
|
|
-zoom, zoom, -1, 1);
|
|
view->setCamera(app.cam);
|
|
view->setPostProcessingEnabled(false);
|
|
|
|
// Pre-calculate the layout transform for each object in a centered 2D grid arrangement.
|
|
const float rowStart = (App::ROW_COUNT - 1) * 0.5f;
|
|
for (int i = 0; i < App::OBJECT_COUNT; ++i) {
|
|
int row = i / App::OBJECT_COUNT_PER_ROW;
|
|
int col = i % App::OBJECT_COUNT_PER_ROW;
|
|
|
|
// Calculate number of items in this row to center it horizontally.
|
|
// Usually equal to OBJECT_COUNT_PER_ROW, except for the last partial row.
|
|
int colCountForThisRow = std::min(
|
|
App::OBJECT_COUNT - (row * App::OBJECT_COUNT_PER_ROW),
|
|
App::OBJECT_COUNT_PER_ROW);
|
|
float colStart = (colCountForThisRow - 1) * 0.5f;
|
|
|
|
auto s = math::mat4f::scaling(math::float3(0.4f, 0.4f, 0.4f));
|
|
auto t = math::mat4f::translation(
|
|
math::float3(-colStart + (col * 1.0f), rowStart - (row * 1.0f), 0.0f));
|
|
app.objectData[i].baseTransform = t * s;
|
|
}
|
|
|
|
if (engine->isAsynchronousModeEnabled()) {
|
|
// Build a pipeline for asynchronous operations.
|
|
app.onLoadImageComplete = [&app](void* user) {
|
|
// Load this once as it's universal across all objects
|
|
app.createMaterial();
|
|
// Initiate loading multiple renderables at the same time.
|
|
app.startLoadingOneRenderable();
|
|
app.startLoadingOneRenderable();
|
|
app.startLoadingOneRenderable();
|
|
app.startLoadingOneRenderable();
|
|
app.startLoadingOneRenderable();
|
|
};
|
|
app.onCreateTextureComplete = [&app](Texture* tex, void* user) {
|
|
app.updateTexture(user, app.onTextureUpdateComplete);
|
|
};
|
|
app.onTextureUpdateComplete = [&app](Texture* tex, void* user) {
|
|
app.createMaterialInstance(user);
|
|
app.textureReady(user);
|
|
};
|
|
app.onCreateVertexBufferComplete = [&app](VertexBuffer* vb, void* user) {
|
|
app.updateVertexBuffer(user, app.onVertexBufferUpdateComplete);
|
|
};
|
|
app.onVertexBufferUpdateComplete = [&app](VertexBuffer* vb, void* user) {
|
|
app.vertexBufferReady(user);
|
|
};
|
|
app.onCreateIndexBufferComplete = [&app](IndexBuffer* ib, void* user) {
|
|
app.updateIndexBuffer(user, app.onIndexBufferUpdateComplete);
|
|
};
|
|
app.onIndexBufferUpdateComplete = [&app](IndexBuffer* ib, void* user) {
|
|
app.indexBufferReady(user);
|
|
};
|
|
|
|
// Start the chain of asynchronous operations.
|
|
app.loadImage(app.onLoadImageComplete);
|
|
} else {
|
|
// Load an image and a material once as they are shared across all objects
|
|
app.loadImage();
|
|
app.createMaterial();
|
|
// Load renderables synchronously
|
|
for (int i = 0; i < App::OBJECT_COUNT; ++i) {
|
|
void* data = &app.objectData[i];
|
|
app.createTexture(data);
|
|
app.updateTexture(data);
|
|
app.createMaterialInstance(data);
|
|
app.createVertexBuffer(data);
|
|
app.updateVertexBuffer(data);
|
|
app.createIndexBuffer(data);
|
|
app.updateIndexBuffer(data);
|
|
app.createRenderable(data);
|
|
}
|
|
}
|
|
};
|
|
|
|
auto cleanup = [&app](Engine* engine, View*, Scene*) {
|
|
// We set this flag to guard against accessing resources (textures/buffers) inside
|
|
// completion callbacks after cleanup.
|
|
app.shuttingDown = true;
|
|
|
|
for (int i = 0; i < App::OBJECT_COUNT; ++i) {
|
|
auto& data = app.objectData[i];
|
|
if (data.renderable) {
|
|
engine->destroy(data.renderable);
|
|
}
|
|
if (data.matInstance) {
|
|
engine->destroy(data.matInstance);
|
|
}
|
|
if (data.ib) {
|
|
engine->destroy(data.ib);
|
|
}
|
|
if (data.vb) {
|
|
engine->destroy(data.vb);
|
|
}
|
|
if (data.tex) {
|
|
engine->destroy(data.tex);
|
|
}
|
|
}
|
|
if (app.mat) {
|
|
engine->destroy(app.mat);
|
|
}
|
|
if (app.skybox) {
|
|
engine->destroy(app.skybox);
|
|
}
|
|
if (app.camera) {
|
|
engine->destroyCameraComponent(app.camera);
|
|
EntityManager::get().destroy(app.camera);
|
|
}
|
|
};
|
|
|
|
FilamentApp::get().animate([&app](Engine* engine, View* view, double now) {
|
|
auto& tm = engine->getTransformManager();
|
|
for (int i = 0; i < App::OBJECT_COUNT; ++i) {
|
|
auto& data = app.objectData[i];
|
|
if (!data.renderable) {
|
|
continue; // Skip updating transform for renderables that are not loaded yet.
|
|
}
|
|
auto r = math::mat4f::rotation(now, math::float3(0, 0, 1));
|
|
tm.setTransform(tm.getInstance(data.renderable), data.baseTransform * r);
|
|
}
|
|
});
|
|
|
|
FilamentApp::get().run(config, setup, cleanup);
|
|
|
|
return 0;
|
|
}
|