/* * Copyright (C) 2024 Huawei Device Co., Ltd. * 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 "SceneJS.h" #include "JsObjectCache.h" #include "LightJS.h" #include "MaterialJS.h" #include "MeshResourceJS.h" #include "NodeJS.h" #include "ParamParsing.h" #include "Promise.h" #include "RenderContextJS.h" #include "SamplerJS.h" #include "nodejstaskqueue.h" static constexpr BASE_NS::Uid IO_QUEUE { "be88e9a0-9cd8-45ab-be48-937953dc258f" }; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __SCENE_ADAPTER__ #include "3d_widget_adapter_log.h" #include "scene_adapter/scene_adapter.h" #endif // LEGACY COMPATIBILITY start #include #include #include #include #include <3d/ecs/components/name_component.h> #include <3d/ecs/components/node_component.h> #include "nodejstaskqueue.h" // fix names to match "ye olde" implementation // the bug that unnamed nodes stops hierarchy creation also still exists, works around that issue too. void Fixnames(SCENE_NS::IScene::Ptr scene) { struct rr { uint32_t id_ = 1; // not actual tree, but map of entities, and their children. BASE_NS::unordered_map> tree; BASE_NS::vector roots; CORE3D_NS::INodeComponentManager* cm; CORE3D_NS::INameComponentManager* nm; rr(SCENE_NS::IScene::Ptr scene) { CORE_NS::IEcs::Ptr ecs = scene->GetInternalScene()->GetEcsContext().GetNativeEcs(); cm = CORE_NS::GetManager(*ecs); nm = CORE_NS::GetManager(*ecs); fix(); } void scan() { const auto count = cm->GetComponentCount(); // collect nodes and their children. tree.reserve(cm->GetComponentCount()); for (auto i = 0; i < count; i++) { auto enti = cm->GetEntity(i); // add node to our list. (if not yet added) tree.insert({ enti, {} }); auto parent = cm->Get(i).parent; if (CORE_NS::EntityUtil::IsValid(parent)) { tree[parent].push_back(enti); } else { // no parent, so it's a "root" roots.push_back(enti); } } } void recurse(CORE_NS::Entity id) { CORE3D_NS::NameComponent c = nm->Get(id); if (c.name.empty()) { // create a name for unnamed node. c.name = "Unnamed Node "; c.name += BASE_NS::to_string(id_++); nm->Set(id, c); } for (auto c : tree[id]) { recurse(c); } } void fix() { scan(); for (auto i : roots) { id_ = 1; /*// force root node name to match legacy by default. CORE3D_NS::NameComponent c; c.name = "rootNode_"; nm->Set(i, c);*/ for (auto c : tree[i]) { recurse(c); } } } } r(scene); } // LEGACY COMPATIBILITY end using IntfPtr = BASE_NS::shared_ptr; using IntfWeakPtr = BASE_NS::weak_ptr; void SceneJS::Init(napi_env env, napi_value exports) { using namespace NapiApi; auto loadFun = [](napi_env e, napi_callback_info cb) -> napi_value { FunctionContext<> fc(e, cb); return SceneJS::Load(fc); }; auto getDefaultRenderContextFun = [](napi_env e, napi_callback_info cb) -> napi_value { FunctionContext<> fc(e, cb); return RenderContextJS::GetDefaultContext(e); }; napi_property_descriptor props[] = { // static methods napi_property_descriptor{"load", nullptr, loadFun, nullptr, nullptr, nullptr, (napi_property_attributes)(napi_static | napi_default_method)}, napi_property_descriptor{"getDefaultRenderContext", nullptr, getDefaultRenderContextFun, nullptr, nullptr, nullptr, (napi_property_attributes)(napi_static | napi_default_method)}, // properties GetSetProperty("renderMode"), GetSetProperty("environment"), GetProperty("animations"), // animations GetProperty("root"), // scene methods Method, SceneJS, &SceneJS::GetNode>("getNodeByPath"), Method, SceneJS, &SceneJS::GetResourceFactory>("getResourceFactory"), Method, SceneJS, &SceneJS::Dispose>("destroy"), // SceneResourceFactory methods Method, SceneJS, &SceneJS::CreateCamera>("createCamera"), Method, SceneJS, &SceneJS::CreateLight>("createLight"), Method, SceneJS, &SceneJS::CreateNode>("createNode"), Method, SceneJS, &SceneJS::CreateTextNode>("createTextNode"), Method, SceneJS, &SceneJS::CreateMaterial>( "createMaterial"), Method, SceneJS, &SceneJS::CreateShader>("createShader"), Method, SceneJS, &SceneJS::CreateImage>("createImage"), Method, SceneJS, &SceneJS::CreateSampler>("createSampler"), Method, SceneJS, &SceneJS::CreateEnvironment>("createEnvironment"), Method, SceneJS, &SceneJS::CreateScene>("createScene"), Method, SceneJS, &SceneJS::ImportNode>("importNode"), Method, SceneJS, &SceneJS::ImportScene>("importScene"), Method, SceneJS, &SceneJS::RenderFrame>("renderFrame"), Method, SceneJS, &SceneJS::CreateMeshResource>("createMesh"), Method, SceneJS, &SceneJS::CreateGeometry>("createGeometry"), Method, SceneJS, &SceneJS::CreateComponent>("createComponent"), Method, SceneJS, &SceneJS::GetComponent>("getComponent"), Method, SceneJS, &SceneJS::GetRenderContext>("getRenderContext"), }; napi_value func; auto status = napi_define_class(env, "Scene", NAPI_AUTO_LENGTH, BaseObject::ctor(), nullptr, sizeof(props) / sizeof(props[0]), props, &func); napi_set_named_property(env, exports, "Scene", func); NapiApi::MyInstanceState* mis; NapiApi::MyInstanceState::GetInstance(env, reinterpret_cast(&mis)); if (mis) { mis->StoreCtor("Scene", func); } } void SceneJS::RegisterEnums(NapiApi::Object exports) { napi_value v; NapiApi::Object en(exports.GetEnv()); napi_create_uint32(en.GetEnv(), static_cast(SCENE_NS::RenderMode::IF_DIRTY), &v); en.Set("RENDER_WHEN_DIRTY", v); napi_create_uint32(en.GetEnv(), static_cast(SCENE_NS::RenderMode::ALWAYS), &v); en.Set("RENDER_CONTINUOUSLY", v); napi_create_uint32(en.GetEnv(), static_cast(SCENE_NS::RenderMode::MANUAL), &v); en.Set("RENDER_MANUALLY", v); exports.Set("RenderMode", en); } napi_value SceneJS::Load(NapiApi::FunctionContext<>& ctx) { const auto env = ctx.Env(); auto promise = Promise(env); // A SceneJS instance keeps a NodeJS task queue acquired, but we're in a static method creating a SceneJS. // Acquire the JS queue before invoking the JS task. Release it only after the scene is created (in the JS task). const auto jsQ = GetOrCreateNodeTaskQueue(env); auto queueRefCount = interface_cast(jsQ); if (queueRefCount) { queueRefCount->Acquire(); } else { return promise.Reject("Error creating a JS task queue"); } BASE_NS::string uri = ExtractUri(ctx); if (uri.empty()) { uri = "scene://empty"; } // make sure slashes are correct.. *eh* for (;;) { auto t = uri.find_first_of('\\'); if (t == BASE_NS::string::npos) { break; } uri[t] = '/'; } auto massageScene = [](SCENE_NS::IScene::Ptr scene) -> SCENE_NS::IScene::Ptr { if (!scene || !scene->RenderConfiguration()->GetValue()) { return {}; } // Make sure there's a valid root node scene->GetInternalScene()->GetEcsContext().CreateUnnamedRootNode(); // LEGACY COMPATIBILITY start Fixnames(scene); // LEGACY COMPATIBILITY end auto& obr = META_NS::GetObjectRegistry(); AddScene(&obr, scene); return scene; }; auto convertToJs = [promise, queueRefCount = BASE_NS::move(queueRefCount)](SCENE_NS::IScene::Ptr scene) mutable { if (!scene) { promise.Reject("Scene creation failed"); return; } const auto env = promise.Env(); auto jsscene = CreateFromNativeInstance(env, scene, PtrType::STRONG, {}); const auto sceneJs = jsscene.GetJsWrapper(); sceneJs->renderMan_ = scene->CreateObject(SCENE_NS::ClassId::RenderResourceManager).GetResult(); auto curenv = jsscene.Get("environment"); if (curenv.IsUndefinedOrNull()) { // setup default env NapiApi::Object argsIn(env); argsIn.Set("name", "DefaultEnv"); auto res = sceneJs->CreateEnvironment(jsscene, argsIn); res.Set("backgroundType", NapiApi::Value(env, 1)); // image.. but with null. jsscene.Set("environment", res); } for (auto&& c : scene->GetCameras().GetResult()) { c->RenderingPipeline()->SetValue(SCENE_NS::CameraPipeline::FORWARD); c->ColorTargetCustomization()->SetValue( { SCENE_NS::ColorFormat { BASE_NS::BASE_FORMAT_R16G16B16A16_SFLOAT } }); } const auto result = jsscene.ToNapiValue(); #ifdef __SCENE_ADAPTER__ // set SceneAdapter auto sceneAdapter = std::make_shared(); sceneAdapter->SetSceneObj(interface_pointer_cast(scene)); sceneJs->scene_ = sceneAdapter; #endif promise.Resolve(result); queueRefCount->Release(); }; auto sceneManager = CreateSceneManager(uri); if (!sceneManager) { return promise.Reject("Creating scene manager failed"); } auto engineQ = META_NS::GetTaskQueueRegistry().GetTaskQueue(ENGINE_THREAD); /* REMOVED DUE TO SCENE API CHANGE if (uri.ends_with(".scene2")) { const auto scene = SCENE_NS::LoadScene(sceneManager->GetContext(), uri); META_NS::AddFutureTaskOrRunDirectly(engineQ, [=]() { return massageScene(scene); }).Then(BASE_NS::move(convertToJs), jsQ); } else {*/ sceneManager->CreateScene(uri).Then(BASE_NS::move(massageScene), engineQ).Then(BASE_NS::move(convertToJs), jsQ); //} return promise; } SCENE_NS::ISceneManager::Ptr SceneJS::CreateSceneManager(BASE_NS::string_view uri) { auto& objRegistry = META_NS::GetObjectRegistry(); auto objContext = interface_pointer_cast(objRegistry.GetDefaultObjectContext()); if (uri.ends_with(".scene2")) { const auto renderContext = SCENE_NS::GetBuildArg(objContext, "RenderContext"); if (!renderContext || !renderContext->GetRenderer()) { LOG_E("Unable to configure file resource manager for loading scene files: render context missing"); return {}; } auto& fileManager = renderContext->GetRenderer()->GetEngine().GetFileManager(); fileManager.RegisterPath("project", ExtractPathToProject(uri), true); } return objRegistry.Create(SCENE_NS::ClassId::SceneManager, objContext); } BASE_NS::string_view SceneJS::ExtractPathToProject(BASE_NS::string_view uri) { // Assume the scene file is in a folder that is at the root of the project. // ExtractPathToProject("schema://path/to/PROJECT/assets/default.scene2") == "schema://path/to/PROJECT" const auto secondToLastSlashPos = uri.find_last_of('/', uri.find_last_of('/') - 1); return uri.substr(0, secondToLastSlashPos); } napi_value SceneJS::Dispose(NapiApi::FunctionContext<>& ctx) { DisposeNative(nullptr); #ifdef __SCENE_ADAPTER__ if (scene_) { scene_->Deinit(); } #endif return {}; } void SceneJS::DisposeNative(void*) { if (!disposed_) { disposed_ = true; LOG_V("SCENE_JS::DisposeNative"); NapiApi::Object scen(env_); napi_value tmp; napi_create_external( env_, static_cast(this), [](napi_env env, void* data, void* finalize_hint) { // do nothing. }, nullptr, &tmp); scen.Set("SceneJS", tmp); napi_value scene = scen.ToNapiValue(); // dispose active environment if (auto env = environmentJS_.GetObject()) { NapiApi::Function func = env.Get("destroy"); if (func) { func.Invoke(env, 1, &scene); } } environmentJS_.Reset(); // dispose all cameras/env/etcs. while (!strongDisposables_.empty()) { auto it = strongDisposables_.begin(); auto token = it->first; auto obj = it->second.GetObject(); if (obj) { // "detaching" the nodes let's the destroy/dispose method release fully. auto isNode = static_cast(obj.GetRoot()->GetInstanceImpl(NodeImpl::ID)); if (isNode && isNode->IsAttached()) { // it's a node, and it's attached. so detach. isNode->Attached(false); } auto size = strongDisposables_.size(); NapiApi::Function func = obj.Get("destroy"); if (func) { func.Invoke(obj, 1, &scene); } if (size == strongDisposables_.size()) { LOG_E("Dispose function didn't dispose."); strongDisposables_.erase(strongDisposables_.begin()); } } else { strongDisposables_.erase(strongDisposables_.begin()); } } // dispose while (!disposables_.empty()) { auto env = disposables_.begin()->second.GetObject(); if (env) { auto size = disposables_.size(); NapiApi::Function func = env.Get("destroy"); if (func) { func.Invoke(env, 1, &scene); } if (size == disposables_.size()) { LOG_E("Weak ref dispose function didn't dispose."); disposables_.erase(disposables_.begin()); } } else { disposables_.erase(disposables_.begin()); } } if (auto nativeScene = interface_pointer_cast(GetNativeObject())) { UnsetNativeObject(); auto r = nativeScene->RenderConfiguration()->GetValue(); if (r) { r->Environment()->SetValue(nullptr); r.reset(); } } FlushScenes(); // Release the js task queue. auto tq = META_NS::GetTaskQueueRegistry().GetTaskQueue(JS_THREAD_DEP); if (auto p = interface_cast(tq)) { p->Release(); // check if we can safely de-init here also. if (p->IsReleased()) { // destroy and unregister the queue. DeinitNodeTaskQueue(); } } resources_.reset(); renderContextJS_.Reset(); } } void* SceneJS::GetInstanceImpl(uint32_t id) { if (id == SceneJS::ID) return this; return nullptr; } void SceneJS::Finalize(napi_env env) { DisposeNative(nullptr); BaseObject::Finalize(env); } napi_value SceneJS::CreateNode( const NapiApi::FunctionContext<>& variableCtx, BASE_NS::string_view jsClassName, META_NS::ObjectId classId) { // Take only the first arg of the variable-length context. auto ctx = NapiApi::FunctionContext { variableCtx.GetEnv(), variableCtx.GetInfo(), NapiApi::ArgCount::PARTIAL }; const auto env = ctx.GetEnv(); auto promise = Promise(env); auto sceneNodeParameters = ctx.Arg<0>(); auto convertToJs = [promise, jsClassName = BASE_NS::string { jsClassName }, sceneRef = NapiApi::StrongRef { ctx.This() }, paramRef = NapiApi::StrongRef { sceneNodeParameters }](SCENE_NS::INode::Ptr node) mutable { const auto env = sceneRef.GetEnv(); napi_value args[] = { sceneRef.GetValue(), paramRef.GetValue() }; const auto result = CreateFromNativeInstance(env, jsClassName, node, PtrType::WEAK, args); if (auto node = result.GetJsWrapper()) { node->Attached(true); } promise.Resolve(result.ToNapiValue()); }; const auto nodePath = ExtractNodePath(sceneNodeParameters); auto scene = ctx.This().GetNative(); const auto jsQ = META_NS::GetTaskQueueRegistry().GetTaskQueue(JS_THREAD_DEP); scene->CreateNode(nodePath, classId).Then(BASE_NS::move(convertToJs), jsQ); return promise; } void SceneJS::AddScene(META_NS::IObjectRegistry* obr, SCENE_NS::IScene::Ptr scene) { if (!obr) { return; } auto params = interface_pointer_cast(obr->GetDefaultObjectContext()); if (!params) { return; } auto duh = params->GetArrayProperty("Scenes"); if (!duh) { return; } duh->AddValue(interface_pointer_cast(scene)); } SceneJS::SceneJS(napi_env e, napi_callback_info i) : BaseObject(e, i) { LOG_V("SceneJS ++"); // Acquire the js task queue. (make sure that as long as we have a scene, the nodejstaskqueue is useable) if (auto jsQueue = interface_cast(GetOrCreateNodeTaskQueue(e))) { jsQueue->Acquire(); } env_ = e; // store.. NapiApi::FunctionContext fromJs(e, i); if (fromJs) { if (auto obj = fromJs.Arg<0>().valueOrDefault()) { if (obj.GetJsWrapper()) { renderContextJS_ = NapiApi::StrongRef(obj); } } } if (renderContextJS_.IsEmpty()) { renderContextJS_ = NapiApi::StrongRef(e, RenderContextJS::GetDefaultContext(e)); } if (renderContextJS_.IsEmpty()) { LOG_E("No render context"); } if (auto wrapper = renderContextJS_.GetObject().GetJsWrapper()) { resources_ = wrapper->GetResources(); } if (!fromJs) { // okay internal create. we will receive the object after. return; } // SceneJS should be created by "Scene.Load" only. // so this never happens. } void SceneJS::FlushScenes() { ExecSyncTask([]() { auto& obr = META_NS::GetObjectRegistry(); if (auto params = interface_pointer_cast(obr.GetDefaultObjectContext())) { if (auto duh = params->GetArrayProperty("Scenes")) { for (auto i = 0; i < duh->GetSize();) { auto w = duh->GetValueAt(i); if (w.lock() == nullptr) { duh->RemoveAt(i); } else { i++; } } } } return META_NS::IAny::Ptr {}; }); } SceneJS::~SceneJS() { LOG_V("SceneJS --"); DisposeNative(nullptr); // flush all null scene objects here too. FlushScenes(); if (!GetNativeObject()) { return; } } napi_value SceneJS::GetNode(NapiApi::FunctionContext& ctx) { // verify that path starts from "correct root" and then let the root node handle the rest. NapiApi::Object meJs(ctx.This()); NapiApi::Object root = meJs.Get("root"); BASE_NS::string rootName = root.Get("name"); NapiApi::Function func = root.Get("getNodeByPath"); BASE_NS::string path = ctx.Arg<0>(); if (path.empty() || (path == BASE_NS::string_view("/")) || (path == rootName)) { // empty or '/' or "exact rootnodename". so return root return root.ToNapiValue(); } // remove the "root nodes name", if given (make sure it also matches though..) auto pos = 0; if (path[0] != '/') { pos = path.find('/', 0); BASE_NS::string_view step = path.substr(0, pos); if (!step.empty() && (step != rootName)) { // root not matching return ctx.GetNull(); } } if (pos != BASE_NS::string_view::npos) { path = path.substr(pos + 1); } if (path.empty()) { // after removing the root node name // nothing left in path, so return root. return root.ToNapiValue(); } napi_value newpath = ctx.GetString(path); if (newpath) { return func.Invoke(root, 1, &newpath); } return ctx.GetNull(); } napi_value SceneJS::GetRoot(NapiApi::FunctionContext<>& ctx) { if (auto scene = interface_cast(GetNativeObject())) { SCENE_NS::INode::Ptr root = scene->GetRootNode().GetResult(); NapiApi::StrongRef sceneRef { ctx.This() }; if (!sceneRef.GetObject().GetNative()) { LOG_F("INVALID SCENE!"); } NapiApi::Object argJS(ctx.GetEnv()); napi_value args[] = { sceneRef.GetObject().ToNapiValue(), argJS.ToNapiValue() }; // Store a weak ref, as these are owned by the scene. auto js = CreateFromNativeInstance(ctx.GetEnv(), root, PtrType::WEAK, args); if (auto node = js.GetJsWrapper()) { node->Attached(true); } return js.ToNapiValue(); } return ctx.GetUndefined(); } napi_value SceneJS::GetEnvironment(NapiApi::FunctionContext<>& ctx) { if (auto scene = interface_cast(GetNativeObject())) { SCENE_NS::IEnvironment::Ptr environment; auto rc = scene->RenderConfiguration()->GetValue(); if (rc) { environment = rc->Environment()->GetValue(); } if (environment) { NapiApi::StrongRef sceneRef { ctx.This() }; if (!sceneRef.GetObject().GetNative()) { LOG_F("INVALID SCENE!"); } NapiApi::Env env(ctx.Env()); NapiApi::Object argJS(env); napi_value args[] = { sceneRef.GetObject().ToNapiValue(), argJS.ToNapiValue() }; environmentJS_ = NapiApi::StrongRef(CreateFromNativeInstance(env, environment, PtrType::WEAK, args)); return environmentJS_.GetValue(); } } return ctx.GetNull(); } void SceneJS::SetEnvironment(NapiApi::FunctionContext& ctx) { NapiApi::Object envObj = ctx.Arg<0>(); if (!envObj) { return; } if (auto currentlySet = environmentJS_.GetObject()) { if (envObj.StrictEqual(currentlySet)) { // setting the exactly the same environment. do nothing. return; } } environmentJS_ = NapiApi::StrongRef(envObj); SCENE_NS::IEnvironment::Ptr environment = envObj.GetNative(); if (auto scene = interface_cast(GetNativeObject())) { auto rc = scene->RenderConfiguration()->GetValue(); if (rc) { rc->Environment()->SetValue(environment); } } } // resource factory napi_value SceneJS::GetResourceFactory(NapiApi::FunctionContext<>& ctx) { // just return this. as scene is the factory also. return ctx.This().ToNapiValue(); } NapiApi::Object SceneJS::CreateEnvironment(NapiApi::Object scene, NapiApi::Object argsIn) { napi_env env = scene.GetEnv(); napi_value args[] = { scene.ToNapiValue(), argsIn.ToNapiValue() }; auto nativeScene = scene.GetNative(); auto nativeEnv = nativeScene->CreateObject(SCENE_NS::ClassId::Environment).GetResult(); return CreateFromNativeInstance(env, nativeEnv, PtrType::STRONG, args); } napi_value SceneJS::CreateEnvironment(NapiApi::FunctionContext& ctx) { const auto env = ctx.GetEnv(); napi_value args[] = { ctx.This().ToNapiValue(), ctx.Arg<0>().ToNapiValue() }; return Promise(env).Resolve(CreateEnvironment(ctx.This(), ctx.Arg<0>()).ToNapiValue()); } napi_value SceneJS::CreateCamera(NapiApi::FunctionContext& ctx) { const auto env = ctx.GetEnv(); auto promise = Promise(env); const auto sceneJs = ctx.This(); auto scene = sceneJs.GetNative(); auto params = NapiApi::Object { ctx.Arg<0>() }; const auto path = ExtractNodePath(params); // renderPipeline is (at the moment of writing) an undocumented param. Check the API docs and usage. // Remove this, if it has been added to the API. Else if it's not used anywhere, remove the implementation. auto pipeline = uint32_t(SCENE_NS::CameraPipeline::LIGHT_FORWARD); if (const auto renderPipelineJs = params.Get("renderPipeline")) { pipeline = NapiApi::Value(env, renderPipelineJs); } // Don't create the camera asynchronously. There's a race condition, and we need to deactivate it immediately. // Otherwise we get tons of render validation issues. const auto camera = scene->CreateNode(path, SCENE_NS::ClassId::CameraNode).GetResult(); camera->SetActive(false); camera->RenderingPipeline()->SetValue(SCENE_NS::CameraPipeline(pipeline)); napi_value args[] = { sceneJs.ToNapiValue(), params.ToNapiValue() }; // Store a weak ref, as these are owned by the scene. const auto result = CreateFromNativeInstance(env, camera, PtrType::WEAK, args); if (auto node = result.GetJsWrapper()) { node->Attached(true); } return promise.Resolve(result.ToNapiValue()); } napi_value SceneJS::CreateLight(NapiApi::FunctionContext& ctx) { uint32_t lightType = ctx.Arg<1>(); BASE_NS::string ctorName; if (lightType == BaseLight::DIRECTIONAL) { ctorName = "DirectionalLight"; } else if (lightType == BaseLight::POINT) { ctorName = "PointLight"; } else if (lightType == BaseLight::SPOT) { ctorName = "SpotLight"; } else { return Promise(ctx.GetEnv()).Reject("Unknown light type given"); } return CreateNode(ctx, ctorName, SCENE_NS::ClassId::LightNode); } napi_value SceneJS::CreateNode(NapiApi::FunctionContext& ctx) { return CreateNode(ctx, "Node", SCENE_NS::ClassId::Node); } napi_value SceneJS::CreateTextNode(NapiApi::FunctionContext& ctx) { return CreateNode(ctx, "TextNode", SCENE_NS::ClassId::TextNode); } napi_value SceneJS::CreateMaterial(NapiApi::FunctionContext& ctx) { const auto env = ctx.GetEnv(); auto promise = Promise(env); uint32_t type = ctx.Arg<1>(); auto convertToJs = [promise, type, sceneRef = NapiApi::StrongRef(ctx.This()), paramRef = NapiApi::StrongRef(ctx.Arg<0>())](SCENE_NS::IMaterial::Ptr material) mutable { const auto env = promise.Env(); if (type == BaseMaterial::SHADER) { META_NS::SetValue(material->Type(), SCENE_NS::MaterialType::CUSTOM); } napi_value args[] = { sceneRef.GetValue(), paramRef.GetValue() }; const auto result = CreateFromNativeInstance(env, material, PtrType::STRONG, args); promise.Resolve(result.ToNapiValue()); }; const auto scene = interface_pointer_cast(GetNativeObject()); const auto jsQ = META_NS::GetTaskQueueRegistry().GetTaskQueue(JS_THREAD_DEP); scene->CreateObject(SCENE_NS::ClassId::Material).Then(BASE_NS::move(convertToJs), jsQ); return promise; } napi_value SceneJS::CreateScene(NapiApi::FunctionContext<>& ctx) { return Load(ctx); } napi_value SceneJS::ImportNode(NapiApi::FunctionContext& ctx) { BASE_NS::string name = ctx.Arg<0>(); NapiApi::Object nnode = ctx.Arg<1>(); NapiApi::Object nparent = ctx.Arg<2>(); auto scene = interface_cast(GetNativeObject()); if (!nnode || !scene) { return ctx.GetNull(); } SCENE_NS::INode::Ptr node = nnode.GetNative(); SCENE_NS::INode::Ptr parent; if (nparent) { parent = nparent.GetNative(); } else { parent = scene->GetRootNode().GetResult(); } if (auto import = interface_cast(parent)) { auto importedNode = import->ImportChild(node).GetResult(); if (!name.empty()) { if (auto named = interface_cast(importedNode)) { named->Name()->SetValue(name); } } NapiApi::StrongRef sceneRef { ctx.This() }; NapiApi::Object argJS(ctx.GetEnv()); napi_value args[] = { sceneRef.GetObject().ToNapiValue(), argJS.ToNapiValue() }; // Store a weak ref, as these are owned by the scene. auto jsNode = CreateFromNativeInstance(ctx.GetEnv(), importedNode, PtrType::WEAK, args); if (auto jsNodeWrapper = jsNode.GetJsWrapper()) { jsNodeWrapper->Attached(true); } return jsNode.ToNapiValue(); } return ctx.GetNull(); } napi_value SceneJS::ImportScene(NapiApi::FunctionContext& ctx) { BASE_NS::string name = ctx.Arg<0>(); NapiApi::Object nextScene = ctx.Arg<1>(); NapiApi::Object nparent = ctx.Arg<2>(); auto scene = interface_cast(GetNativeObject()); if (!nextScene || !scene) { return ctx.GetNull(); } SCENE_NS::IScene::Ptr extScene = nextScene.GetNative(); SCENE_NS::INode::Ptr parent; if (nparent) { parent = nparent.GetNative(); } else { parent = scene->GetRootNode().GetResult(); } if (auto import = interface_cast(parent)) { auto result = import->ImportChildScene(extScene, name).GetResult(); NapiApi::StrongRef sceneRef { ctx.This() }; NapiApi::Object argJS(ctx.GetEnv()); napi_value args[] = { sceneRef.GetObject().ToNapiValue(), argJS.ToNapiValue() }; // Store a weak ref, as these are owned by the scene. return CreateFromNativeInstance(ctx.GetEnv(), result, PtrType::WEAK, args).ToNapiValue(); } return ctx.GetNull(); } napi_value SceneJS::GetRenderMode(NapiApi::FunctionContext<>& ctx) { auto scene = interface_cast(GetNativeObject()); if (!scene) { return ctx.GetUndefined(); } return ctx.GetNumber(uint32_t(scene->GetRenderMode().GetResult())); } void SceneJS::SetRenderMode(NapiApi::FunctionContext& ctx) { auto scene = interface_cast(GetNativeObject()); if (!scene) { return; } uint32_t v = ctx.Arg<0>(); if (v >= static_cast(SCENE_NS::RenderMode::IF_DIRTY) && v <= static_cast(SCENE_NS::RenderMode::MANUAL)) { scene->SetRenderMode(static_cast(v)).Wait(); } } napi_value SceneJS::RenderFrame(NapiApi::FunctionContext<>& ctx) { if (ctx.ArgCount() > 1) { CORE_LOG_E("render frame %d", __LINE__); return ctx.GetBoolean(false); } #ifdef __SCENE_ADAPTER__ auto sceneAdapter = std::static_pointer_cast(scene_); if (sceneAdapter) { auto scene = interface_cast(GetNativeObject()); if (!scene) { return ctx.GetBoolean(false); } // set RenderMode based on Arg<0>.alwaysRender NapiApi::FunctionContext paramsCtx(ctx); NapiApi::Object params = paramsCtx.Arg<0>(); bool alwaysRender = params.IsUndefinedOrNull("alwaysRender") ? true : params.Get("alwaysRender"); if (currentAlwaysRender_ != alwaysRender) { currentAlwaysRender_ = alwaysRender; scene->SetRenderMode(static_cast(alwaysRender)); } sceneAdapter->SetNeedsRepaint(false); sceneAdapter->RenderFrame(false); } else { return ctx.GetBoolean(false); } #else auto scene = interface_cast(GetNativeObject()); if (!scene) { return ctx.GetBoolean(false); } // todo: fix this // NapiApi::Object params = ctx.Arg<0>(); // auto alwaysRender = params ? params.Get("alwaysRender") : true; if (auto sc = scene->GetInternalScene()) { sc->RenderFrame(); } #endif return ctx.GetBoolean(true); } napi_value SceneJS::CreateComponent(NapiApi::FunctionContext& ctx) { auto env = ctx.GetEnv(); auto promise = Promise(env); if (ctx.ArgCount() > 2) { // 2: arg num return promise.Reject("Invalid number of arguments"); } NapiApi::Object nnode = ctx.Arg<0>(); BASE_NS::string name = ctx.Arg<1>(); auto scene = interface_cast(GetNativeObject()); if (!scene || !nnode || name.empty()) { return promise.Reject("Invalid parameters given"); } auto node = nnode.GetNative(); if (auto comp = scene->CreateComponent(node, name).GetResult()) { NapiApi::Env env(ctx.GetEnv()); NapiApi::Object argJS(env); NapiApi::WeakRef sceneRef { ctx.This() }; napi_value args[] = { sceneRef.GetValue(), argJS.ToNapiValue() }; return promise.Resolve( CreateFromNativeInstance(env, "SceneComponent", comp, PtrType::WEAK, args).ToNapiValue()); } return promise.Reject("Could not create component"); } napi_value SceneJS::GetComponent(NapiApi::FunctionContext& ctx) { if (ctx.ArgCount() > 2) { // 2: arg num return ctx.GetNull(); } NapiApi::Object nnode = ctx.Arg<0>(); BASE_NS::string name = ctx.Arg<1>(); if (!nnode) { return ctx.GetNull(); } if (auto attach = nnode.GetNative()) { if (auto cont = attach->GetAttachmentContainer(false)) { if (auto comp = cont->FindByName(name)) { NapiApi::Env env(ctx.GetEnv()); NapiApi::Object argJS(env); NapiApi::WeakRef sceneRef { ctx.This() }; napi_value args[] = { sceneRef.GetValue(), argJS.ToNapiValue() }; return CreateFromNativeInstance(env, "SceneComponent", comp, PtrType::WEAK, args).ToNapiValue(); } } } return ctx.GetNull(); } napi_value SceneJS::GetRenderContext(NapiApi::FunctionContext<>& ctx) { return renderContextJS_.GetValue(); } napi_value SceneJS::CreateMeshResource(NapiApi::FunctionContext& ctx) { auto env = ctx.GetEnv(); auto promise = Promise(env); auto geometry = GeometryDefinition::GeometryDefinition::FromJs(ctx.Arg<1>()); if (!geometry) { return promise.Reject("Invalid geometry definition given"); } // Piggyback the native geometry definition inside the resource param. Need to ditch smart pointers for the ride. napi_value geometryNapiValue; napi_create_external(ctx.Env(), geometry.release(), nullptr, nullptr, &geometryNapiValue); NapiApi::Object resourceParams = ctx.Arg<0>(); resourceParams.Set("GeometryDefinition", geometryNapiValue); napi_value args[] = { ctx.This().ToNapiValue(), resourceParams.ToNapiValue() }; const auto meshResource = META_NS::GetObjectRegistry().Create(SCENE_NS::ClassId::MeshResource); const auto result = CreateFromNativeInstance(env, meshResource, PtrType::STRONG, args); return promise.Resolve(result.ToNapiValue()); } napi_value SceneJS::CreateGeometry(NapiApi::FunctionContext& ctx) { const auto env = ctx.GetEnv(); auto promise = Promise(env); const auto sceneJs = ctx.This(); auto params = NapiApi::Object { ctx.Arg<0>() }; auto meshRes = NapiApi::Object { ctx.Arg<1>() }; auto tro = meshRes.GetRoot(); if (!tro) { return promise.Reject("Invalid mesh resource given"); } const auto meshResourceJs = static_cast(tro->GetInstanceImpl(MeshResourceJS::ID)); if (!meshResourceJs) { return promise.Reject("Invalid mesh resource given"); } // We don't use futures here, but rather just GetResult everything immediately. // Otherwise there's a race condition for a deadlock. auto scene = sceneJs.GetNative(); const auto path = ExtractNodePath(params); auto meshNode = scene->CreateNode(path, SCENE_NS::ClassId::MeshNode).GetResult(); if (auto access = interface_pointer_cast(meshNode)) { const auto mesh = meshResourceJs->CreateMesh(); access->SetMesh(mesh).GetResult(); napi_value args[] = { sceneJs.ToNapiValue(), params.ToNapiValue() }; // Store a weak ref, as these are owned by the scene. const auto result = CreateFromNativeInstance(env, meshNode, PtrType::WEAK, args); if (auto node = result.GetJsWrapper()) { node->Attached(true); } return promise.Resolve(result.ToNapiValue()); } return promise.Reject("Geometry node creation failed. Is the given node path unique and valid?"); } napi_value SceneJS::CreateShader(NapiApi::FunctionContext& ctx) { const auto env = ctx.GetEnv(); auto promise = Promise(env); NapiApi::Object resourceParams = ctx.Arg<0>(); if (!resourceParams) { return promise.Reject("Invalid scene resource shader parameters given"); } auto uri = ExtractUri(resourceParams.Get("uri")); if (uri.empty()) { auto u = resourceParams.Get("uri"); uri = ExtractUri(u); } if (uri.empty()) { return promise.Reject("Invalid scene resource Shader parameters given"); } auto scene = interface_pointer_cast(GetNativeObject()); auto convertToJs = [promise, uri, sceneRef = NapiApi::StrongRef(ctx.This()), paramRef = NapiApi::StrongRef(resourceParams)](SCENE_NS::IShader::Ptr shader) mutable { if (!shader) { CORE_LOG_E("Fail to load shader but do not return %s", uri.c_str()); } const auto env = promise.Env(); napi_value args[] = { sceneRef.GetValue(), paramRef.GetValue() }; NapiApi::Object parms(env, args[1]); napi_value null; napi_get_null(env, &null); parms.Set("Material", null); // not bound to anything... const auto result = CreateFromNativeInstance(env, shader, PtrType::STRONG, args); promise.Resolve(result.ToNapiValue()); }; auto jsQ = META_NS::GetTaskQueueRegistry().GetTaskQueue(JS_THREAD_DEP); renderMan_->LoadShader(uri).Then(BASE_NS::move(convertToJs), jsQ); return promise; } // To pass LoadResult between tasks with Future::Then. META_TYPE(BASE_NS::shared_ptr); napi_value SceneJS::CreateImage(NapiApi::FunctionContext& ctx) { if (!renderContextJS_.IsEmpty()) { if (auto context = renderContextJS_.GetObject().GetJsWrapper()) { NapiApi::Function f(ctx.GetEnv(), renderContextJS_.GetObject().Get("createImage")); auto param = ctx.Arg<0>().ToNapiValue(); return f.Invoke(renderContextJS_.GetObject(), 1, ¶m); } } return ctx.GetUndefined(); } napi_value SceneJS::CreateSampler(NapiApi::FunctionContext& ctx) { return Promise(ctx.GetEnv()).Resolve(SamplerJS::CreateRawJsObject(ctx.GetEnv())); } napi_value SceneJS::GetAnimations(NapiApi::FunctionContext<>& ctx) { auto scene = ctx.This().GetNative(); if (!scene) { return ctx.GetUndefined(); } BASE_NS::vector animRes; ExecSyncTask([scene, &animRes]() { animRes = scene->GetAnimations().GetResult(); return META_NS::IAny::Ptr {}; }); napi_value tmp; auto status = napi_create_array_with_length(ctx.Env(), animRes.size(), &tmp); size_t i = 0; napi_value args[] = { ctx.This().ToNapiValue(), NapiApi::Object(ctx.Env()).ToNapiValue() }; for (const auto& node : animRes) { auto val = CreateFromNativeInstance(ctx.Env(), node, PtrType::STRONG, args); status = napi_set_element(ctx.Env(), tmp, i++, val.ToNapiValue()); } return tmp; } void SceneJS::DisposeHook(uintptr_t token, NapiApi::Object obj) { disposables_[token] = { obj }; } void SceneJS::ReleaseDispose(uintptr_t token) { auto it = disposables_.find(token); if (it != disposables_.end()) { it->second.Reset(); disposables_.erase(it->first); } } void SceneJS::StrongDisposeHook(uintptr_t token, NapiApi::Object obj) { strongDisposables_[token] = NapiApi::StrongRef(obj); } void SceneJS::ReleaseStrongDispose(uintptr_t token) { auto it = strongDisposables_.find(token); if (it != strongDisposables_.end()) { it->second.Reset(); strongDisposables_.erase(it->first); } }