diff --git a/examples/loaders/collada_loader.cpp b/examples/loaders/collada_loader.cpp index 15f6d450..f33b4e93 100644 --- a/examples/loaders/collada_loader.cpp +++ b/examples/loaders/collada_loader.cpp @@ -23,36 +23,41 @@ int main() { auto ambientLight = AmbientLight::create(0xffffff, 0.2f); scene->add(ambientLight); - auto dirLight = DirectionalLight::create(0xffffff, 1.0f); dirLight->position.set(1, 1, 1); scene->add(dirLight); ColladaLoader loader; - auto stormTrooper = loader.load(std::string(DATA_FOLDER) + "/models/collada/stormtrooper/stormtrooper.dae"); + std::unique_ptr mixer; + auto stormTrooper = loadAsync([&]() -> std::shared_ptr { + auto model = loader.load(std::string(DATA_FOLDER) + "/models/collada/stormtrooper/stormtrooper.dae"); + if (!model) return nullptr; - if (!stormTrooper) { - std::cerr << "Failed to load model\n"; - return 1; - } - stormTrooper->rotateZ(math::PI); - scene->add(stormTrooper); + model->rotateZ(math::PI); - Box3 bb; - bb.setFromObject(*stormTrooper); + Box3 bb; + bb.setFromObject(*model); - controls.target = bb.getCenter(); - controls.update(); + controls.target = bb.getCenter(); + controls.update(); - std::unique_ptr mixer; - if (!stormTrooper->animations.empty()) { - std::cout << "Loaded " << stormTrooper->animations.size() << " animation clip(s)." << std::endl; - mixer = std::make_unique(*stormTrooper); - mixer->clipAction(stormTrooper->animations.front())->play(); + if (!model->animations.empty()) { + std::cout << "Loaded " << model->animations.size() << " animation clip(s)." << std::endl; + mixer = std::make_unique(*model); + mixer->clipAction(model->animations.front())->play(); + } + + auto skeletonHelper = SkeletonHelper::create(*model); + scene->add(skeletonHelper); + return model; + }); + + if (!stormTrooper) { + std::cerr << "Failed to load model\n"; + return 1; } - auto skeletonHelper = SkeletonHelper::create(*stormTrooper); - scene->add(skeletonHelper); + scene->add(stormTrooper); Clock clock; canvas.animate([&] { diff --git a/examples/loaders/obj_loader.cpp b/examples/loaders/obj_loader.cpp index 05a473b1..1af1d934 100644 --- a/examples/loaders/obj_loader.cpp +++ b/examples/loaders/obj_loader.cpp @@ -1,4 +1,5 @@ +#include #include using namespace threepp; @@ -30,19 +31,22 @@ int main() { camera->position.set(0, 100, 150); OBJLoader loader; - auto obj1 = loader.load(std::string(DATA_FOLDER) + "/models/obj/female02/female02.obj"); + auto obj1 = loadAsync(loader, std::string(DATA_FOLDER) + "/models/obj/female02/female02.obj"); obj1->position.x = -30; scene->add(obj1); - TextureLoader tl; - auto tex = tl.load(std::string(DATA_FOLDER) + "/textures/uv_grid_opengl.jpg", ColorSpace::sRGB); - auto obj2 = loader.load(std::string(DATA_FOLDER) + "/models/obj/female02/female02.obj", false); - obj2->position.x = 30; - obj2->traverseType([tex](Mesh& child) { - auto m = MeshPhongMaterial::create(); - m->map = tex; - child.setMaterial(m); + auto obj2 = loadAsync([] { + auto tex = TextureLoader().load(std::string(DATA_FOLDER) + "/textures/uv_grid_opengl.jpg", ColorSpace::sRGB); + auto model = OBJLoader().load(std::string(DATA_FOLDER) + "/models/obj/female02/female02.obj", false); + model->traverseType([tex](Mesh& child) { + auto m = MeshPhongMaterial::create(); + m->map = tex; + child.setMaterial(m); + }); + return model; }); + obj2->position.x = 30; + scene->add(obj2); createAndAddLights(*scene); diff --git a/examples/projects/Crane3R/main.cpp b/examples/projects/Crane3R/main.cpp index 1a0e316c..7f0828be 100644 --- a/examples/projects/Crane3R/main.cpp +++ b/examples/projects/Crane3R/main.cpp @@ -7,10 +7,6 @@ #include "threepp/threepp.hpp" #include "utility/Angle.hpp" -#ifndef __EMSCRIPTEN__ -#include -#endif - using namespace threepp; using namespace kine; @@ -135,34 +131,28 @@ int main() { .addLink(Vector3::Z() * 5.2) .build(); - TaskManager tm; - -#ifndef __EMSCRIPTEN__ - std::shared_ptr crane; - auto future = std::async([&] { - crane = Crane3R::create(); - crane->setTargetValues(asAngles(kine.meanAngles(), Angle::Repr::DEG)); - crane->traverseType([](Mesh& m) { + Crane3R* crane = nullptr; + auto craneGroup = loadAsync([&kine]() -> std::shared_ptr { + auto c = Crane3R::create(); + c->setTargetValues(asAngles(kine.meanAngles(), Angle::Repr::DEG)); + c->traverseType([](Mesh& m) { m.castShadow = true; }); - - tm.invokeLater([&, crane] { - hud.remove(handle); - scene->add(crane); - endEffectorHelper->visible = true; - }); + return c; }); -#else - auto crane = Crane3R::create(); - crane->setTargetValues(asAngles(kine.meanAngles(), Angle::Repr::DEG)); - crane->traverseType([](Mesh& m) { - m.castShadow = true; + + craneGroup->onLoaded([&](AsyncGroup& g) { + for (auto* child : g.children) { + if (auto* c = dynamic_cast(child)) { + crane = c; + break; + } + } + hud.remove(handle); + endEffectorHelper->visible = true; }); - hud.remove(handle); - scene->add(crane); - endEffectorHelper->visible = true; -#endif + scene->add(craneGroup); canvas.onWindowResize([&](WindowSize size) { camera->aspect = size.aspect(); @@ -198,8 +188,6 @@ int main() { canvas.animate([&] { const auto dt = clock.getDelta(); - tm.handleTasks(); - transformControls.visible = targetHelper->visible; renderer->clear(); @@ -237,8 +225,4 @@ int main() { hud.render(); } }); - -#ifndef __EMSCRIPTEN__ - future.get(); -#endif } diff --git a/examples/projects/Youbot/youbot_kine.cpp b/examples/projects/Youbot/youbot_kine.cpp index f579f1ae..b8d4f97e 100644 --- a/examples/projects/Youbot/youbot_kine.cpp +++ b/examples/projects/Youbot/youbot_kine.cpp @@ -9,8 +9,6 @@ #include "threepp/extras/imgui/ImguiContext.hpp" #include "threepp/objects/TextSprite.hpp" -#include - using namespace threepp; using namespace kine; @@ -115,31 +113,40 @@ int main() { HUD hud(*renderer); hud.add(textHandle).setNormalizedPosition({0.5, 0.5}); - TaskManager tm; - - std::shared_ptr youbot; + Youbot* youbot = nullptr; std::unique_ptr keyController; - auto loadFuture = std::async(std::launch::async, [&] { - try { - youbot = Youbot::create(std::string(DATA_FOLDER) + "/models/collada/youbot.dae"); + auto youbotGroup = loadAsync([path = std::string(DATA_FOLDER) + "/models/collada/youbot.dae"]() -> std::shared_ptr { + auto y = Youbot::create(path); + auto wrapper = Group::create(); + wrapper->add(std::shared_ptr(y.release())); + return wrapper; + }); + + youbotGroup->onLoaded([&](AsyncGroup& g) { + g.traverse([&](Object3D& obj) { + if (!youbot) { + if (auto* y = dynamic_cast(&obj)) { + youbot = y; + } + } + }); + if (youbot) { youbot->add(targetHelper); youbot->add(endEffectorHelper); endEffectorHelper->visible = true; keyController = std::make_unique(*youbot); - - tm.invokeLater([&] { - canvas.addKeyListener(*keyController); - scene->add(youbot); - textHandle.setText("Use WASD keys to steer robot"); - hud.getStoredOptions(textHandle)->setNormalizedPosition(0, 0).setVerticalAlignment(HUD::VerticalAlignment::ABOVE).setHorizontalAlignment(HUD::HorizontalAlignment::LEFT); - }); - } catch (const std::exception& e) { - tm.invokeLater([&, msg = std::string(e.what())] { - textHandle.setText("Error: " + msg); - }); + canvas.addKeyListener(*keyController); + textHandle.setText("Use WASD keys to steer robot"); + hud.getStoredOptions(textHandle)->setNormalizedPosition(0, 0) + .setVerticalAlignment(HUD::VerticalAlignment::ABOVE) + .setHorizontalAlignment(HUD::HorizontalAlignment::LEFT); + } else { + textHandle.setText("Error loading model"); } }); + scene->add(youbotGroup); + canvas.onWindowResize([&](WindowSize size) { camera->aspect = size.aspect(); camera->updateProjectionMatrix(); @@ -173,8 +180,6 @@ int main() { canvas.animate([&] { const auto dt = clock.getDelta(); - tm.handleTasks(); - renderer->clear(); renderer->render(*scene, *camera); @@ -206,7 +211,4 @@ int main() { hud.render(); }); - - loadFuture.get(); - } diff --git a/examples/vulkan/CMakeLists.txt b/examples/vulkan/CMakeLists.txt index 863fecaf..c17bdf7c 100644 --- a/examples/vulkan/CMakeLists.txt +++ b/examples/vulkan/CMakeLists.txt @@ -4,4 +4,7 @@ add_example(NAME "vulkan_showcase" LINK_IMGUI) add_example(NAME "vulkan_lights" LINK_IMGUI) add_example(NAME "vulkan_fog" LINK_IMGUI) add_example(NAME "vulkan_ocean" LINK_IMGUI) -add_example(NAME "vulkan_bistro" LINK_IMGUI) + +if (THREEPP_WITH_FBX) + add_example(NAME "vulkan_bistro" LINK_IMGUI) +endif () \ No newline at end of file diff --git a/examples/vulkan/vulkan_gltf_samples.cpp b/examples/vulkan/vulkan_gltf_samples.cpp index 56322e58..eb5c3c45 100644 --- a/examples/vulkan/vulkan_gltf_samples.cpp +++ b/examples/vulkan/vulkan_gltf_samples.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include using namespace threepp; @@ -99,34 +98,47 @@ int main(int argc, char** argv) { controls.enableKeys = false; controls.update(); - struct LoadedGltf { - std::shared_ptr scene; - std::vector> animations; - }; - GLTFLoader loader; int currentModel = -1; - std::shared_ptr loadedModel; + std::shared_ptr loadedModel; std::unique_ptr mixer; - std::vector> clips; - std::future modelFuture; - bool loadPending = false; + + auto fitCamera = [&](Object3D& obj) { + Box3 bbox; + bbox.setFromObject(obj); + auto center = bbox.getCenter(); + auto size = bbox.getSize(); + float maxDim = std::max({size.x, size.y, size.z}); + float dist = maxDim * 1.5f / std::tan(camera.fov * 0.5f * math::PI / 180.f); + + controls.target.copy(center); + camera.position.set(center.x, center.y, center.z + dist); + camera.nearPlane = dist * 0.01f; + camera.farPlane = dist * 100.f; + camera.updateProjectionMatrix(); + controls.update(); + }; auto loadModel = [&](int idx) { if (idx < 0 || idx >= static_cast(models.size())) return; - if (loadPending) return; + + if (loadedModel) { + scene.remove(*loadedModel); + loadedModel.reset(); + } + mixer.reset(); currentModel = idx; - loadPending = true; auto path = models[idx].path; - std::cout << "Loading: " << models[idx].name << " (" << path << ")" << std::endl; + auto name = models[idx].name; + std::cout << "Loading: " << name << " (" << path << ")" << std::endl; - modelFuture = std::async(std::launch::async, [&loader, path, name = models[idx].name]() -> LoadedGltf { + loadedModel = loadAsync([&loader, path, name]() -> std::shared_ptr { try { auto result = loader.load(path); if (!result || !result->scene) { std::cerr << "Load failed '" << name << "'" << std::endl; - return {}; + return nullptr; } auto& root = result->scene; bool hasMesh = false; @@ -137,30 +149,28 @@ int main(int argc, char** argv) { }); if (!hasMesh) { std::cerr << "Skipping '" << name << "': no mesh geometry" << std::endl; - return {}; + return nullptr; } - return {root, std::move(result->animations)}; + root->animations = result->animations; + return root; } catch (const std::exception& e) { std::cerr << "Load failed '" << name << "': " << e.what() << std::endl; - return {}; + return nullptr; } }); - }; - auto fitCamera = [&](Object3D& obj) { - Box3 bbox; - bbox.setFromObject(obj); - auto center = bbox.getCenter(); - auto size = bbox.getSize(); - float maxDim = std::max({size.x, size.y, size.z}); - float dist = maxDim * 1.5f / std::tan(camera.fov * 0.5f * math::PI / 180.f); + loadedModel->onLoaded([&](AsyncGroup& g) { + fitCamera(g); + if (!g.animations.empty()) { + mixer = std::make_unique(g); + mixer->clipAction(g.animations.front())->play(); + std::cout << "Playing animation: " << g.animations.front()->name() + << " (" << g.animations.size() << " clip(s))" << std::endl; + } + std::cout << "Loaded: " << models[currentModel].name << std::endl; + }); - controls.target.copy(center); - camera.position.set(center.x, center.y, center.z + dist); - camera.nearPlane = dist * 0.01f; - camera.farPlane = dist * 100.f; - camera.updateProjectionMatrix(); - controls.update(); + scene.add(loadedModel); }; loadModel(0); @@ -190,7 +200,7 @@ int main(int argc, char** argv) { ImGui::Separator(); ImGui::Text("Model: %s", currentModel >= 0 ? models[currentModel].name.c_str() : "none"); - if (loadPending) ImGui::Text("Loading..."); + if (loadedModel && loadedModel->isLoading()) ImGui::Text("Loading..."); ImGui::Text("Left/Right arrows to browse"); if (ImGui::CollapsingHeader("Models")) { @@ -255,31 +265,6 @@ int main(int argc, char** argv) { fpsFrames = 0; } - if (loadPending && modelFuture.valid() && - modelFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - auto loaded = modelFuture.get(); - loadPending = false; - if (loadedModel) { - scene.remove(*loadedModel); - loadedModel.reset(); - } - mixer.reset(); - clips.clear(); - loadedModel = loaded.scene; - if (loadedModel) { - scene.add(loadedModel); - fitCamera(*loadedModel); - clips = std::move(loaded.animations); - if (!clips.empty()) { - mixer = std::make_unique(*loadedModel); - mixer->clipAction(clips.front())->play(); - std::cout << "Playing animation: " << clips.front()->name() - << " (" << clips.size() << " clip(s))" << std::endl; - } - std::cout << "Loaded: " << models[currentModel].name << std::endl; - } - } - if (mixer) mixer->update(dt); controls.update(); diff --git a/examples/vulkan/vulkan_ocean.cpp b/examples/vulkan/vulkan_ocean.cpp index ef504815..7bb9d2de 100644 --- a/examples/vulkan/vulkan_ocean.cpp +++ b/examples/vulkan/vulkan_ocean.cpp @@ -194,43 +194,29 @@ int main() { ocean->params.textureSize = kFftSize; scene.add(ocean); - // Gunnerus research vessel (~28 m × 9 m). Loaded synchronously at - // startup — 31 MB binary glTF takes ~1 s. Re-scaled to a known length so - // the hydrodynamic sample points (fore/aft/port/starboard) are - // physically meaningful regardless of the asset's source unit. constexpr float kBoatLength = 28.0f; constexpr float kBoatBeam = 9.0f; GLTFLoader gltfLoader; - auto boat = Group::create(); - auto gltf = gltfLoader.load(std::string(DATA_FOLDER) + "/models/gltf/Gunnerus.glb"); - if (!gltf || !gltf->scene) { - std::cerr << "Failed to load Gunnerus.glb" << std::endl; - - boat->add(Mesh::create(BoxGeometry::create(kBoatBeam, 5.f, kBoatLength), - MeshStandardMaterial::create({{"color", Color::red}}))); + auto boat = loadAsync([&gltfLoader]() -> std::shared_ptr { + auto gltf = gltfLoader.load(std::string(DATA_FOLDER) + "/models/gltf/Gunnerus.glb"); + if (!gltf || !gltf->scene) { + std::cerr << "Failed to load Gunnerus.glb" << std::endl; + auto fallback = Group::create(); + fallback->add(Mesh::create(BoxGeometry::create(kBoatBeam, 5.f, kBoatLength), + MeshStandardMaterial::create({{"color", Color::red}}))); + return fallback; + } - } else { auto innerAsset = gltf->scene; - - boat->add(innerAsset); { Box3 bbox; bbox.setFromObject(*innerAsset); Vector3 size; bbox.getSize(size); - // If the asset is longer in X than Z, its longitudinal axis is +X - // not +Z. Rotate so its long axis aligns with Z to match our heading - // convention (yaw = 0 → translate along +Z). if (size.x > size.z) { innerAsset->rotateY(-math::PI / 2.f); bbox.setFromObject(*innerAsset); bbox.getSize(size); } - // Bow direction can't be inferred from bbox alone — both ends of - // the longest axis look the same from outside. Empirically Gunnerus - // ends up with bow at −Z after the auto-rotation, so W key (which - // translates +Z) reads as "go backward". Add a 180° flip so the - // visual nose points along the heading direction. If you load a - // different asset whose bow ends up at +Z naturally, drop this. innerAsset->rotateY(math::PI); const float maxExtent = std::max({size.x, size.y, size.z}); if (maxExtent > 0.f) { @@ -238,8 +224,11 @@ int main() { innerAsset->scale.set(s, s, s); } } - } + auto group = Group::create(); + group->add(innerAsset); + return group; + }); scene.add(boat); BoatState bs; BoatInput bi; diff --git a/include/threepp/loaders/AsyncGroup.hpp b/include/threepp/loaders/AsyncGroup.hpp new file mode 100644 index 00000000..536cbb88 --- /dev/null +++ b/include/threepp/loaders/AsyncGroup.hpp @@ -0,0 +1,56 @@ + +#ifndef THREEPP_ASYNCGROUP_HPP +#define THREEPP_ASYNCGROUP_HPP + +#include "threepp/loaders/Loader.hpp" +#include "threepp/objects/Group.hpp" + +#include +#include + +namespace threepp { + + class AsyncGroup: public Group { + + public: + using LoadedCallback = std::function; + + [[nodiscard]] std::string type() const override; + + [[nodiscard]] bool isLoaded() const; + + [[nodiscard]] bool isLoading() const; + + void onLoaded(LoadedCallback cb); + + void updateMatrixWorld(bool force = false) override; + + static std::shared_ptr create(); + + void deliverResult(std::shared_ptr result); + void setLoading(bool value); + + ~AsyncGroup() override; + + protected: + std::shared_ptr createDefault() override; + + private: + AsyncGroup(); + + struct Impl; + std::unique_ptr pimpl_; + }; + + std::shared_ptr loadAsync(std::function()> loadFn); + + template + std::shared_ptr loadAsync(LoaderT& loader, Args&&... args) { + return loadAsync([&loader, ...args = std::forward(args)]() -> std::shared_ptr { + return loader.load(args...); + }); + } + +}// namespace threepp + +#endif//THREEPP_ASYNCGROUP_HPP diff --git a/include/threepp/loaders/ModelLoader.hpp b/include/threepp/loaders/ModelLoader.hpp index c1e96638..44098b2d 100644 --- a/include/threepp/loaders/ModelLoader.hpp +++ b/include/threepp/loaders/ModelLoader.hpp @@ -2,6 +2,7 @@ #ifndef THREEPP_MODELLOADER_HPP #define THREEPP_MODELLOADER_HPP +#include "threepp/loaders/AsyncGroup.hpp" #include "threepp/loaders/Loader.hpp" #include "threepp/objects/Group.hpp" @@ -17,6 +18,10 @@ namespace threepp { // Supported: .obj, .dae, .gltf, .glb, .stl [[nodiscard]] std::shared_ptr load(const std::filesystem::path& path) override; + // Async variant — returns an empty AsyncGroup immediately. + // Children appear automatically once loading completes. + [[nodiscard]] std::shared_ptr loadAsync(const std::filesystem::path& path); + // Propagates to inner loaders that have a file-level up-axis (Collada, // USD). Use when this ModelLoader is being driven by an outer system // (URDF/SDF/MJCF) that owns the coordinate frame. diff --git a/include/threepp/threepp.hpp b/include/threepp/threepp.hpp index 1a1dd01c..c51b2bac 100644 --- a/include/threepp/threepp.hpp +++ b/include/threepp/threepp.hpp @@ -27,6 +27,7 @@ #include "threepp/core/Object3D.hpp" #include "threepp/core/Raycaster.hpp" +#include "threepp/loaders/AsyncGroup.hpp" #include "threepp/objects/Group.hpp" #include "threepp/objects/HUD.hpp" #include "threepp/objects/InstancedMesh.hpp" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 81446f72..67d4747c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -107,6 +107,7 @@ set(publicHeaders "threepp/helpers/SkeletonHelper.hpp" "threepp/helpers/SpotLightHelper.hpp" + "threepp/loaders/AsyncGroup.hpp" "threepp/loaders/loaders.hpp" "threepp/loaders/AssimpLoader.hpp" "threepp/loaders/CubeTextureLoader.hpp" @@ -369,6 +370,7 @@ set(sources "threepp/input/PeripheralsEventSource.cpp" + "threepp/loaders/AsyncGroup.cpp" "threepp/loaders/ColladaLoader.cpp" "threepp/loaders/DDSLoader.cpp" "threepp/loaders/FontLoader.cpp" diff --git a/src/threepp/loaders/AsyncGroup.cpp b/src/threepp/loaders/AsyncGroup.cpp new file mode 100644 index 00000000..fcae01ef --- /dev/null +++ b/src/threepp/loaders/AsyncGroup.cpp @@ -0,0 +1,131 @@ + +#include "threepp/loaders/AsyncGroup.hpp" + +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +using namespace threepp; + +namespace { + + struct LoadContext { + std::weak_ptr weak; + std::function()> fn; + }; + + void executeLoad(LoadContext* ctx) { + std::shared_ptr result; + try { + result = ctx->fn(); + } catch (...) {} + + if (auto ag = ctx->weak.lock()) { + ag->deliverResult(std::move(result)); + } + } + +}// namespace + +struct AsyncGroup::Impl { + std::mutex mutex; + std::vector> pendingChildren; + std::vector callbacks; + std::atomic hasPending{false}; + std::atomic loaded{false}; + std::atomic loading{false}; +}; + +AsyncGroup::AsyncGroup(): pimpl_(std::make_unique()) {} + +AsyncGroup::~AsyncGroup() = default; + +std::string AsyncGroup::type() const { + return "AsyncGroup"; +} + +bool AsyncGroup::isLoaded() const { + return pimpl_->loaded.load(std::memory_order_acquire); +} + +bool AsyncGroup::isLoading() const { + return pimpl_->loading.load(std::memory_order_acquire); +} + +void AsyncGroup::deliverResult(std::shared_ptr result) { + std::lock_guard lock(pimpl_->mutex); + if (result) { + pimpl_->pendingChildren.push_back(std::move(result)); + pimpl_->hasPending.store(true, std::memory_order_release); + } + pimpl_->loading = false; +} + +void AsyncGroup::setLoading(bool value) { + pimpl_->loading = value; +} + +void AsyncGroup::onLoaded(LoadedCallback cb) { + if (pimpl_->loaded) { + cb(*this); + } else { + pimpl_->callbacks.push_back(std::move(cb)); + } +} + +void AsyncGroup::updateMatrixWorld(bool force) { + if (pimpl_->hasPending.load(std::memory_order_acquire)) { + std::lock_guard lock(pimpl_->mutex); + for (auto& child : pimpl_->pendingChildren) { + for (auto& anim : child->animations) { + this->animations.push_back(anim); + } + this->add(child); + } + pimpl_->pendingChildren.clear(); + pimpl_->loaded = true; + pimpl_->hasPending.store(false, std::memory_order_release); + + for (auto& cb : pimpl_->callbacks) { + cb(*this); + } + pimpl_->callbacks.clear(); + } + Group::updateMatrixWorld(force); +} + +std::shared_ptr AsyncGroup::create() { + return std::shared_ptr(new AsyncGroup()); +} + +std::shared_ptr AsyncGroup::createDefault() { + return create(); +} + +std::shared_ptr threepp::loadAsync(std::function()> loadFn) { + auto group = AsyncGroup::create(); + group->setLoading(true); + auto* ctx = new LoadContext{group, std::move(loadFn)}; + +#ifdef __EMSCRIPTEN__ + emscripten_async_call( + [](void* arg) { + auto* c = static_cast(arg); + executeLoad(c); + delete c; + }, + ctx, 0); +#else + std::thread([ctx]() { + executeLoad(ctx); + delete ctx; + }).detach(); +#endif + + return group; +} diff --git a/src/threepp/loaders/ModelLoader.cpp b/src/threepp/loaders/ModelLoader.cpp index ab343d20..bba3c5ec 100644 --- a/src/threepp/loaders/ModelLoader.cpp +++ b/src/threepp/loaders/ModelLoader.cpp @@ -78,6 +78,10 @@ std::shared_ptr ModelLoader::load(const std::filesystem::path& path) { return nullptr; } +std::shared_ptr ModelLoader::loadAsync(const std::filesystem::path& path) { + return threepp::loadAsync(*this, path); +} + ModelLoader& ModelLoader::setIgnoreUpDirection(bool ignore) { ignoreUpDirection_ = ignore; return *this; diff --git a/src/threepp/loaders/OBJLoader.cpp b/src/threepp/loaders/OBJLoader.cpp index 9dda2776..d4e0d74c 100644 --- a/src/threepp/loaders/OBJLoader.cpp +++ b/src/threepp/loaders/OBJLoader.cpp @@ -291,14 +291,14 @@ struct OBJLoader::Impl { std::shared_ptr load(const std::filesystem::path& path, bool tryLoadMtl) { - if (scope.useCache && cache_.contains(path.string())) { + const auto cacheKey = path.string() + (tryLoadMtl ? ":mtl" : ":nomtl"); - auto cached = cache_.at(path.string()); - if (!cached.expired()) { + if (scope.useCache && cache_.contains(cacheKey)) { + + if (auto cached = cache_.at(cacheKey); !cached.expired()) { return cached.lock()->clone(); - } else { - cache_.erase(path.string()); } + cache_.erase(cacheKey); } if (!std::filesystem::exists(path)) { @@ -478,7 +478,7 @@ struct OBJLoader::Impl { std::shared_ptr material; - if (this->materials) { + if (this->materials && tryLoadMtl) { material = this->materials->create(sourceMaterial->name); if (isLine && material && !material->is()) { @@ -549,7 +549,7 @@ struct OBJLoader::Impl { container->add(mesh); } - if (scope.useCache) cache_[path.string()] = container; + if (scope.useCache) cache_[cacheKey] = container; return container; }