UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

305 lines (276 loc) 10.4 kB
/** * the node to rull them all * * @remarks * * The root node is unique in the scene. it is the higest parent, just above the [obj](/docs/nodes/obj) nodes. * * It allows you to control the following scene properties: * * - the scene background (which can be nothing, a plain color, or a texture) * - which camera is used when exporting the scene * - the fog * - an environment map which would override every material's environment map * - a material which would override every other material * - the display of an audio icon in the viewer, which is useful when using sound in your scene, and allowing users to turn it on/off * */ import {TypedBaseManagerNode} from './_Base'; import {BaseObjNodeType} from '../obj/_Base'; import {NodeContext} from '../../poly/NodeContext'; import {ObjNodeChildrenMap} from '../../poly/registers/nodes/Obj'; import {NodeParamsConfig} from '../utils/params/ParamsConfig'; import {BaseNodeType} from '../_Base'; import {HierarchyObjNode} from '../obj/utils/HierarchyController'; import {Constructor, valueof} from '../../../types/GlobalTypes'; import {ROOT_NAME} from '../../scene/utils/ObjectsController'; import {Scene} from 'three'; import {RootAudioController, RootAudioParamConfig} from './utils/Scene/Audio'; import {SceneAutoUpdateParamConfig, SceneAutoUpdateController} from './utils/Scene/AutoUpdate'; import {SceneBackgroundParamConfig, SceneBackgroundController} from './utils/Scene/Background'; import {SceneEnvParamConfig, SceneEnvController} from './utils/Scene/Env'; import {SceneFogParamConfig, SceneFogController} from './utils/Scene/Fog'; import {RootLoadProgressParamConfig, RootLoadProgressController} from './utils/Scene/LoadProgress'; import {SceneMaterialOverrideParamConfig, SceneMaterialOverrideController} from './utils/Scene/MaterialOverride'; import {RootMainCameraController, RootMainCameraParamConfig} from './utils/Scene/RootMainCamera'; import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController'; export const ROOT_NODE_NAME = 'RootNode'; class ObjectsManagerParamsConfig extends RootLoadProgressParamConfig( RootAudioParamConfig( SceneMaterialOverrideParamConfig( SceneEnvParamConfig( SceneFogParamConfig( RootMainCameraParamConfig(SceneBackgroundParamConfig(SceneAutoUpdateParamConfig(NodeParamsConfig))) ) ) ) ) ) {} const ParamsConfig = new ObjectsManagerParamsConfig(); export class RootManagerNode extends TypedBaseManagerNode<ObjectsManagerParamsConfig> { override paramsConfig = ParamsConfig; static override type() { return 'root'; } protected _object: Scene = this._createScene(); private _queuedNodesById: Map<number, BaseObjNodeType> = new Map(); // private _expected_geo_nodes: PolyDictionary<GeoObjNode> = {}; // private _process_queue_start: number = -1; readonly audioController: RootAudioController = new RootAudioController(this); readonly sceneAutoUpdateController: SceneAutoUpdateController = new SceneAutoUpdateController(this); readonly sceneBackgroundController: SceneBackgroundController = new SceneBackgroundController(this); readonly sceneEnvController: SceneEnvController = new SceneEnvController(this); readonly sceneFogController: SceneFogController = new SceneFogController(this); readonly loadProgress: RootLoadProgressController = new RootLoadProgressController(this); readonly sceneMaterialOverrideController: SceneMaterialOverrideController = new SceneMaterialOverrideController( this as any ); readonly mainCameraController: RootMainCameraController = new RootMainCameraController(this); override cook() { // the cook method is necessary here, // with the .endCook() // Without it, the button param of this node would not execute // its callback, as this node would not be perceived as cooking done this.cookController.endCook(); } protected override _childrenControllerContext = NodeContext.OBJ; override initializeNode() { // this.children_controller?.init({dependent: false}); this.params.onParamsCreated('init controllers', () => { this.sceneEnvController.addHooks(); this.sceneBackgroundController.addHooks(); }); this.lifecycle.onChildAdd(this._onChildAdd.bind(this)); this.lifecycle.onChildRemove(this._onChildRemove.bind(this)); } private _createScene() { const scene = new Scene(); scene.name = ROOT_NAME; // DO NOT set matrixAutoUpdate to false // on the scene, as this has too many side effects // and is probably an over optimisation anyway scene.matrixAutoUpdate = true; return scene; } get object() { return this._object; } override createNode<S extends keyof ObjNodeChildrenMap>( nodeClass: S, options?: NodeCreateOptions ): ObjNodeChildrenMap[S]; override createNode<K extends valueof<ObjNodeChildrenMap>>( nodeClass: Constructor<K>, options?: NodeCreateOptions ): K; override createNode<K extends valueof<ObjNodeChildrenMap>>( nodeClass: Constructor<K>, options?: NodeCreateOptions ): K { return super.createNode(nodeClass, options) as K; } override children() { return super.children() as BaseObjNodeType[]; } override nodesByType<K extends keyof ObjNodeChildrenMap>(type: K): ObjNodeChildrenMap[K][] { return super.nodesByType(type) as ObjNodeChildrenMap[K][]; } // multiple_display_flags_allowed() { // return true; // } private _updateScene() { this.sceneAutoUpdateController.update(); this.sceneBackgroundController.update(); this.sceneEnvController.update(); this.sceneFogController.update(); this.sceneMaterialOverrideController.update(); } private _addToQueue(node: BaseObjNodeType) { const id = node.graphNodeId(); if (!this._queuedNodesById.has(id)) { this._queuedNodesById.set(id, node); } return node; } processQueue() { this._updateScene(); const queuedNodesByPath: Map<string, BaseObjNodeType> = new Map(); const paths: string[] = []; this._queuedNodesById.forEach((node, id) => { const fullPath = `_____${node.renderOrder}__${node.path()}`; paths.push(fullPath); queuedNodesByPath.set(fullPath, node); }); this._queuedNodesById.clear(); // const promises = []; for (const path_id of paths) { const node = queuedNodesByPath.get(path_id); if (node) { queuedNodesByPath.delete(path_id); this._addToScene(node); // promises.push(); } } // this._expected_geo_nodes = this._expected_geo_nodes || (await this.expected_loading_geo_nodes_by_id()); // this._process_queue_start = performance.now(); // Promise.all(promises).then(() => { // // Poly.log(`SCENE LOADED '${this.scene.name}`); // // `SCENE LOADED '${this.scene.name}' in ${performance.now() - this._process_queue_start}` // // this.scene().performance().print() // // do the update here if there are no objects to load // // otherwise an empty scene will have a loader that never gets removed // // if (Object.keys(this._expected_geo_nodes).length == 0) { // // this.update_on_all_objects_loaded(); // // } // }); } private _updateObject(node: BaseObjNodeType) { if (!this.scene().loadingController.autoUpdating()) { return this._addToQueue(node); } else { // update 25/01/2022: the light obj nodes, the null obj do not cook when created. // this aims to solve this. // and only once the scene has loaded if (node.isDisplayed() && !node.cookController.isCooking()) { node.compute(); } return this._addToScene(node); } } // // // OBJ PARENTING // // getParentForNode(node: BaseObjNodeType) { if (node.attachableToHierarchy()) { const node_input = node.io.inputs.input(0); if (node_input) { return node_input.childrenGroup(); } else { return this._object; } } else { return null; } } private _addToScene(node: BaseObjNodeType): void { if (node.attachableToHierarchy()) { const parentObject = this.getParentForNode(node); if (parentObject) { // await node.params.eval_all().then((params_eval_key) => { // node.compute(); // }); if (node.usedInScene()) { // I need to query the displayNodeController here, // for geo obj whose display_node is a node without inputs. // Since that node will not be made dirty, it seems that there is // nothing triggering the obj to request it itself. // TODO: investigate if it has a performance cost, or if it could be done // only when scene loads. Or if the displayNodeController itself could be improved // to take care of it itself. // node.compute(); if (node.childrenDisplayController) { node.childrenDisplayController.requestDisplayNodeContainer(); } else { node.compute(); } node.addObjectToParent(parentObject); } else { node.removeObjectFromParent(); // parent_object.remove(node.object); } // node.request_display_node(); } else { // node.compute().then(() => { // // force events and mat to cook and remove the dirty state // // ensure that pickers are cooked // // TODO: although there has been cases with two picker and // // one referencing the other with an expression, and that // // expression be evaluated before the second was created // // which led to an error. This should not happen // node.children_controller.traverse_children((child) => child.setDirty()); // }); } } } private _removeFromScene(node: BaseObjNodeType) { node.removeObjectFromParent(); } areChildrenCooking(): boolean { const children = this.children(); for (const child of children) { if (child.cookController.isCooking() || child.isDisplayNodeCooking()) { return true; } } return false; } // private async expected_loading_geo_nodes_by_id() { // const geo_nodes = this.nodesByType('geo'); // const node_by_id: PolyDictionary<GeoObjNode> = {}; // for (let geo_node of geo_nodes) { // const isDisplayed = await geo_node.isDisplayed(); // if (isDisplayed) { // node_by_id[geo_node.graphNodeId()] = geo_node; // } // } // return node_by_id; // } addToParentTransform(node: HierarchyObjNode) { this._updateObject(node); } removeFromParentTransform(node: HierarchyObjNode) { this._updateObject(node); } private _onChildAdd(node?: BaseNodeType) { if (node) { this._updateObject(node as BaseObjNodeType); } } private _onChildRemove(node?: BaseNodeType) { if (node) { this._removeFromScene(node as BaseObjNodeType); } } }