@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
305 lines (276 loc) • 10.4 kB
text/typescript
/**
* 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);
}
}
}