@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
663 lines (662 loc) • 27.3 kB
JavaScript
import { __decorate } from "../../tslib.es6.js";
import { Observable } from "../../Misc/observable.js";
import { NodeRenderGraphOutputBlock } from "./Blocks/outputBlock.js";
import { FrameGraph } from "../frameGraph.js";
import { GetClass } from "../../Misc/typeStore.js";
import { serialize } from "../../Misc/decorators.js";
import { SerializationHelper } from "../../Misc/decorators.serialization.js";
import { WebRequest } from "../../Misc/webRequest.js";
import { NodeRenderGraphInputBlock } from "./Blocks/inputBlock.js";
import { Tools } from "../../Misc/tools.js";
import { Engine } from "../../Engines/engine.js";
import { NodeRenderGraphBlockConnectionPointTypes } from "./Types/nodeRenderGraphTypes.js";
import { NodeRenderGraphClearBlock } from "./Blocks/Textures/clearBlock.js";
import { NodeRenderGraphObjectRendererBlock } from "./Blocks/Rendering/objectRendererBlock.js";
import { NodeRenderGraphBuildState } from "./nodeRenderGraphBuildState.js";
import { NodeRenderGraphCullObjectsBlock } from "./Blocks/cullObjectsBlock.js";
/**
* Defines a node render graph
*/
export class NodeRenderGraph {
/** @returns the inspector from bundle or global */
_getGlobalNodeRenderGraphEditor() {
// UMD Global name detection from Webpack Bundle UMD Name.
if (typeof NODERENDERGRAPHEDITOR !== "undefined") {
return NODERENDERGRAPHEDITOR;
}
// In case of module let's check the global emitted from the editor entry point.
if (typeof BABYLON !== "undefined" && typeof BABYLON.NodeRenderGraphEditor !== "undefined") {
return BABYLON;
}
return undefined;
}
/**
* Gets the frame graph used by this node render graph
*/
get frameGraph() {
return this._frameGraph;
}
/**
* Gets the scene used by this node render graph
* @returns the scene used by this node render graph
*/
getScene() {
return this._scene;
}
/**
* Creates a new node render graph
* @param name defines the name of the node render graph
* @param scene defines the scene to use to execute the graph
* @param options defines the options to use when creating the graph
*/
constructor(name, scene, options) {
this._buildId = NodeRenderGraph._BuildIdGenerator++;
// eslint-disable-next-line @typescript-eslint/naming-convention
this.BJSNODERENDERGRAPHEDITOR = this._getGlobalNodeRenderGraphEditor();
/**
* Gets or sets data used by visual editor
* @see https://nrge.babylonjs.com
*/
this.editorData = null;
/**
* Gets an array of blocks that needs to be serialized even if they are not yet connected
*/
this.attachedBlocks = [];
/**
* Observable raised when the node render graph is built
*/
this.onBuildObservable = new Observable();
/**
* Observable raised when an error is detected
*/
this.onBuildErrorObservable = new Observable();
/** Gets or sets the RenderGraphOutputBlock used to gather the final node render graph data */
this.outputBlock = null;
this._resizeObserver = null;
this.name = name;
this._scene = scene;
this._engine = scene.getEngine();
options = {
debugTextures: false,
autoConfigure: false,
verbose: false,
rebuildGraphOnEngineResize: true,
autoFillExternalInputs: true,
...options,
};
this._options = options;
this._frameGraph = new FrameGraph(this._scene, options.debugTextures, this);
this._frameGraph.name = name;
if (options.rebuildGraphOnEngineResize) {
this._resizeObserver = this._engine.onResizeObservable.add(() => {
this.build();
});
}
}
/**
* Gets the current class name ("NodeRenderGraph")
* @returns the class name
*/
getClassName() {
return "NodeRenderGraph";
}
/**
* Gets a block by its name
* @param name defines the name of the block to retrieve
* @returns the required block or null if not found
*/
getBlockByName(name) {
let result = null;
for (const block of this.attachedBlocks) {
if (block.name === name) {
if (!result) {
result = block;
}
else {
Tools.Warn("More than one block was found with the name `" + name + "`");
return result;
}
}
}
return result;
}
/**
* Get a block using a predicate
* @param predicate defines the predicate used to find the good candidate
* @returns the required block or null if not found
*/
getBlockByPredicate(predicate) {
for (const block of this.attachedBlocks) {
if (predicate(block)) {
return block;
}
}
return null;
}
/**
* Get all blocks that match a predicate
* @param predicate defines the predicate used to find the good candidate(s)
* @returns the list of blocks found
*/
getBlocksByPredicate(predicate) {
const blocks = [];
for (const block of this.attachedBlocks) {
if (predicate(block)) {
blocks.push(block);
}
}
return blocks;
}
/**
* Gets the list of input blocks attached to this material
* @returns an array of InputBlocks
*/
getInputBlocks() {
const blocks = [];
for (const block of this.attachedBlocks) {
if (block.isInput) {
blocks.push(block);
}
}
return blocks;
}
/**
* Launch the node render graph editor
* @param config Define the configuration of the editor
* @returns a promise fulfilled when the node editor is visible
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
async edit(config) {
return await new Promise((resolve) => {
this.BJSNODERENDERGRAPHEDITOR = this.BJSNODERENDERGRAPHEDITOR || this._getGlobalNodeRenderGraphEditor();
if (typeof this.BJSNODERENDERGRAPHEDITOR == "undefined") {
const editorUrl = config && config.editorURL ? config.editorURL : NodeRenderGraph.EditorURL;
// Load editor and add it to the DOM
Tools.LoadBabylonScript(editorUrl, () => {
this.BJSNODERENDERGRAPHEDITOR = this.BJSNODERENDERGRAPHEDITOR || this._getGlobalNodeRenderGraphEditor();
this._createNodeEditor(config?.nodeRenderGraphEditorConfig);
resolve();
});
}
else {
// Otherwise creates the editor
this._createNodeEditor(config?.nodeRenderGraphEditorConfig);
resolve();
}
});
}
/**
* Creates the node editor window.
* @param additionalConfig Additional configuration for the FGE
*/
_createNodeEditor(additionalConfig) {
const nodeEditorConfig = {
nodeRenderGraph: this,
...additionalConfig,
};
this.BJSNODERENDERGRAPHEDITOR.NodeRenderGraphEditor.Show(nodeEditorConfig);
}
/**
* Build the final list of blocks that will be executed by the "execute" method
*/
build() {
if (!this.outputBlock) {
throw new Error("You must define the outputBlock property before building the node render graph");
}
this._initializeBlock(this.outputBlock);
this._frameGraph.clear();
const state = new NodeRenderGraphBuildState();
state.buildId = this._buildId;
state.verbose = this._options.verbose;
if (this._options.autoFillExternalInputs) {
this._autoFillExternalInputs();
}
// Make sure that one of the object renderer is flagged as the main object renderer
const objectRendererBlocks = this.getBlocksByPredicate((block) => block instanceof NodeRenderGraphObjectRendererBlock);
if (objectRendererBlocks.length > 0 && !objectRendererBlocks.find((block) => block.isMainObjectRenderer)) {
objectRendererBlocks[0].isMainObjectRenderer = true;
}
try {
this.outputBlock.build(state);
this._frameGraph.build();
}
finally {
this._buildId = NodeRenderGraph._BuildIdGenerator++;
if (state.emitErrors(this.onBuildErrorObservable)) {
this.onBuildObservable.notifyObservers(this);
}
}
}
_autoFillExternalInputs() {
const allInputs = this.getInputBlocks();
const shadowLights = [];
for (const light of this._scene.lights) {
if (light.setShadowProjectionMatrix !== undefined) {
shadowLights.push(light);
}
}
let cameraIndex = 0;
let lightIndex = 0;
for (const input of allInputs) {
if (!input.isExternal) {
continue;
}
if (!input.isAnAncestorOfType("NodeRenderGraphOutputBlock")) {
continue;
}
if ((input.type & NodeRenderGraphBlockConnectionPointTypes.TextureAllButBackBuffer) !== 0) {
// nothing to do
}
else if (input.isCamera()) {
const camera = this._scene.cameras[cameraIndex++] || this._scene.cameras[0];
if (!this._scene.cameraToUseForPointers) {
this._scene.cameraToUseForPointers = camera;
}
input.value = camera;
}
else if (input.isObjectList()) {
input.value = { meshes: this._scene.meshes, particleSystems: this._scene.particleSystems };
}
else if (input.isShadowLight()) {
if (lightIndex < shadowLights.length) {
input.value = shadowLights[lightIndex++];
lightIndex = lightIndex % shadowLights.length;
}
}
}
}
/**
* Returns a promise that resolves when the node render graph is ready to be executed
* This method must be called after the graph has been built (NodeRenderGraph.build called)!
* @param timeStep Time step in ms between retries (default is 16)
* @param maxTimeout Maximum time in ms to wait for the graph to be ready (default is 30000)
* @returns The promise that resolves when the graph is ready
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
whenReadyAsync(timeStep = 16, maxTimeout = 30000) {
return this._frameGraph.whenReadyAsync(timeStep, maxTimeout);
}
/**
* Execute the graph (the graph must have been built before!)
*/
execute() {
this._frameGraph.execute();
}
_initializeBlock(node) {
node.initialize();
if (this._options.autoConfigure) {
node.autoConfigure();
}
if (this.attachedBlocks.indexOf(node) === -1) {
this.attachedBlocks.push(node);
}
for (const input of node.inputs) {
const connectedPoint = input.connectedPoint;
if (connectedPoint) {
const block = connectedPoint.ownerBlock;
if (block !== node) {
this._initializeBlock(block);
}
}
}
}
/**
* Clear the current graph
*/
clear() {
this.outputBlock = null;
this.attachedBlocks.length = 0;
}
/**
* Remove a block from the current graph
* @param block defines the block to remove
*/
removeBlock(block) {
const attachedBlockIndex = this.attachedBlocks.indexOf(block);
if (attachedBlockIndex > -1) {
this.attachedBlocks.splice(attachedBlockIndex, 1);
}
if (block === this.outputBlock) {
this.outputBlock = null;
}
}
/**
* Clear the current graph and load a new one from a serialization object
* @param source defines the JSON representation of the graph
* @param merge defines whether or not the source must be merged or replace the current content
*/
parseSerializedObject(source, merge = false) {
if (!merge) {
this.clear();
}
const map = {};
// Create blocks
for (const parsedBlock of source.blocks) {
const blockType = GetClass(parsedBlock.customType);
if (blockType) {
const additionalConstructionParameters = parsedBlock.additionalConstructionParameters;
const block = additionalConstructionParameters
? new blockType("", this._frameGraph, this._scene, ...additionalConstructionParameters)
: new blockType("", this._frameGraph, this._scene);
block._deserialize(parsedBlock);
map[parsedBlock.id] = block;
this.attachedBlocks.push(block);
}
}
// Reconnect teleportation
for (const block of this.attachedBlocks) {
if (block.isTeleportOut) {
const teleportOut = block;
const id = teleportOut._tempEntryPointUniqueId;
if (id) {
const source = map[id];
if (source) {
source.attachToEndpoint(teleportOut);
}
}
}
}
// Connections - Starts with input blocks only (except if in "merge" mode where we scan all blocks)
for (let blockIndex = 0; blockIndex < source.blocks.length; blockIndex++) {
const parsedBlock = source.blocks[blockIndex];
const block = map[parsedBlock.id];
if (!block) {
continue;
}
if (block.inputs.length && parsedBlock.inputs.some((i) => i.targetConnectionName) && !merge) {
continue;
}
this._restoreConnections(block, source, map);
}
// Outputs
if (source.outputNodeId) {
this.outputBlock = map[source.outputNodeId];
}
// UI related info
if (source.locations || (source.editorData && source.editorData.locations)) {
const locations = source.locations || source.editorData.locations;
for (const location of locations) {
if (map[location.blockId]) {
location.blockId = map[location.blockId].uniqueId;
}
}
if (merge && this.editorData && this.editorData.locations) {
locations.concat(this.editorData.locations);
}
if (source.locations) {
this.editorData = {
locations: locations,
};
}
else {
this.editorData = source.editorData;
this.editorData.locations = locations;
}
const blockMap = [];
for (const key in map) {
blockMap[key] = map[key].uniqueId;
}
this.editorData.map = blockMap;
}
this.comment = source.comment;
}
_restoreConnections(block, source, map) {
for (const outputPoint of block.outputs) {
for (const candidate of source.blocks) {
const target = map[candidate.id];
if (!target) {
continue;
}
for (const input of candidate.inputs) {
if (map[input.targetBlockId] === block && input.targetConnectionName === outputPoint.name) {
const inputPoint = target.getInputByName(input.inputName);
if (!inputPoint || inputPoint.isConnected) {
continue;
}
outputPoint.connectTo(inputPoint, true);
this._restoreConnections(target, source, map);
continue;
}
}
}
}
}
/**
* Generate a string containing the code declaration required to create an equivalent of this node render graph
* @returns a string
*/
generateCode() {
let alreadyDumped = [];
const blocks = [];
const uniqueNames = ["const", "var", "let"];
// Gets active blocks
if (this.outputBlock) {
this._gatherBlocks(this.outputBlock, blocks);
}
// Generate
const options = JSON.stringify(this._options);
let codeString = `let nodeRenderGraph = new BABYLON.NodeRenderGraph("${this.name || "render graph"}", scene, ${options});\n`;
for (const node of blocks) {
if (node.isInput && alreadyDumped.indexOf(node) === -1) {
codeString += node._dumpCode(uniqueNames, alreadyDumped) + "\n";
}
}
if (this.outputBlock) {
// Connections
alreadyDumped = [];
codeString += "// Connections\n";
codeString += this.outputBlock._dumpCodeForOutputConnections(alreadyDumped);
// Output nodes
codeString += "// Output nodes\n";
codeString += `nodeRenderGraph.outputBlock = ${this.outputBlock._codeVariableName};\n`;
codeString += `nodeRenderGraph.build();\n`;
}
return codeString;
}
_gatherBlocks(rootNode, list) {
if (list.indexOf(rootNode) !== -1) {
return;
}
list.push(rootNode);
for (const input of rootNode.inputs) {
const connectedPoint = input.connectedPoint;
if (connectedPoint) {
const block = connectedPoint.ownerBlock;
if (block !== rootNode) {
this._gatherBlocks(block, list);
}
}
}
// Teleportation
if (rootNode.isTeleportOut) {
const block = rootNode;
if (block.entryPoint) {
this._gatherBlocks(block.entryPoint, list);
}
}
}
/**
* Clear the current graph and set it to a default state
*/
setToDefault() {
this.clear();
this.editorData = null;
// Source textures
const colorTexture = new NodeRenderGraphInputBlock("Color Texture", this._frameGraph, this._scene, NodeRenderGraphBlockConnectionPointTypes.Texture);
colorTexture.creationOptions.options.samples = 4;
const depthTexture = new NodeRenderGraphInputBlock("Depth Texture", this._frameGraph, this._scene, NodeRenderGraphBlockConnectionPointTypes.TextureDepthStencilAttachment);
depthTexture.creationOptions.options.samples = 4;
// Clear texture
const clear = new NodeRenderGraphClearBlock("Clear", this._frameGraph, this._scene);
clear.clearDepth = true;
clear.clearStencil = true;
colorTexture.output.connectTo(clear.target);
depthTexture.output.connectTo(clear.depth);
// Object list and culling
const camera = new NodeRenderGraphInputBlock("Camera", this._frameGraph, this._scene, NodeRenderGraphBlockConnectionPointTypes.Camera);
const objectList = new NodeRenderGraphInputBlock("Object List", this._frameGraph, this._scene, NodeRenderGraphBlockConnectionPointTypes.ObjectList);
const cull = new NodeRenderGraphCullObjectsBlock("Cull", this._frameGraph, this._scene);
camera.output.connectTo(cull.camera);
objectList.output.connectTo(cull.objects);
// Render objects
const mainRendering = new NodeRenderGraphObjectRendererBlock("Main Rendering", this._frameGraph, this._scene);
camera.output.connectTo(mainRendering.camera);
cull.output.connectTo(mainRendering.objects);
clear.output.connectTo(mainRendering.target);
clear.outputDepth.connectTo(mainRendering.depth);
// Final output
const output = new NodeRenderGraphOutputBlock("Output", this._frameGraph, this._scene);
mainRendering.output.connectTo(output.texture);
this.outputBlock = output;
}
/**
* Makes a duplicate of the current node render graph.
* @param name defines the name to use for the new node render graph
* @returns the new node render graph
*/
clone(name) {
const serializationObject = this.serialize();
const clone = SerializationHelper.Clone(() => new NodeRenderGraph(name, this._scene), this);
clone.name = name;
clone.parseSerializedObject(serializationObject);
clone._buildId = this._buildId;
clone.build();
return clone;
}
/**
* Serializes this node render graph in a JSON representation
* @param selectedBlocks defines the list of blocks to save (if null the whole node render graph will be saved)
* @returns the serialized node render graph object
*/
serialize(selectedBlocks) {
const serializationObject = selectedBlocks ? {} : SerializationHelper.Serialize(this);
serializationObject.editorData = JSON.parse(JSON.stringify(this.editorData)); // Copy
let blocks = [];
if (selectedBlocks) {
blocks = selectedBlocks;
}
else {
serializationObject.customType = "BABYLON.NodeRenderGraph";
if (this.outputBlock) {
serializationObject.outputNodeId = this.outputBlock.uniqueId;
}
}
// Blocks
serializationObject.blocks = [];
for (const block of blocks) {
serializationObject.blocks.push(block.serialize());
}
if (!selectedBlocks) {
for (const block of this.attachedBlocks) {
if (blocks.indexOf(block) !== -1) {
continue;
}
serializationObject.blocks.push(block.serialize());
}
}
return serializationObject;
}
/**
* Disposes the resources
*/
dispose() {
for (const block of this.attachedBlocks) {
block.dispose();
}
this._frameGraph.dispose();
this._frameGraph = undefined;
this._engine.onResizeObservable.remove(this._resizeObserver);
this._resizeObserver = null;
this.attachedBlocks.length = 0;
this.onBuildObservable.clear();
this.onBuildErrorObservable.clear();
}
/**
* Creates a new node render graph set to default basic configuration
* @param name defines the name of the node render graph
* @param scene defines the scene to use
* @param nodeRenderGraphOptions defines options to use when creating the node render graph
* @returns a new NodeRenderGraph
*/
static CreateDefault(name, scene, nodeRenderGraphOptions) {
const renderGraph = new NodeRenderGraph(name, scene, nodeRenderGraphOptions);
renderGraph.setToDefault();
renderGraph.build();
return renderGraph;
}
/**
* Creates a node render graph from parsed graph data
* @param source defines the JSON representation of the node render graph
* @param scene defines the scene to use
* @param nodeRenderGraphOptions defines options to use when creating the node render
* @param skipBuild defines whether to skip building the node render graph (default is true)
* @returns a new node render graph
*/
static Parse(source, scene, nodeRenderGraphOptions, skipBuild = true) {
const renderGraph = SerializationHelper.Parse(() => new NodeRenderGraph(source.name, scene, nodeRenderGraphOptions), source, null);
renderGraph.parseSerializedObject(source);
if (!skipBuild) {
renderGraph.build();
}
return renderGraph;
}
/**
* Creates a node render graph from a snippet saved by the node render graph editor
* @param snippetId defines the snippet to load
* @param scene defines the scene to use
* @param nodeRenderGraphOptions defines options to use when creating the node render graph
* @param nodeRenderGraph defines a node render graph to update (instead of creating a new one)
* @param skipBuild defines whether to skip building the node render graph (default is true)
* @returns a promise that will resolve to the new node render graph
*/
// eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax
static ParseFromSnippetAsync(snippetId, scene, nodeRenderGraphOptions, nodeRenderGraph, skipBuild = true) {
if (snippetId === "_BLANK") {
return Promise.resolve(NodeRenderGraph.CreateDefault("blank", scene, nodeRenderGraphOptions));
}
return new Promise((resolve, reject) => {
const request = new WebRequest();
request.addEventListener("readystatechange", () => {
if (request.readyState == 4) {
if (request.status == 200) {
const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload);
const serializationObject = JSON.parse(snippet.nodeRenderGraph);
if (!nodeRenderGraph) {
nodeRenderGraph = SerializationHelper.Parse(() => new NodeRenderGraph(snippetId, scene, nodeRenderGraphOptions), serializationObject, null);
}
nodeRenderGraph.parseSerializedObject(serializationObject);
nodeRenderGraph.snippetId = snippetId;
try {
if (!skipBuild) {
nodeRenderGraph.build();
}
resolve(nodeRenderGraph);
}
catch (err) {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject(err);
}
}
else {
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
reject("Unable to load the snippet " + snippetId);
}
}
});
request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/"));
request.send();
});
}
}
NodeRenderGraph._BuildIdGenerator = 0;
/** Define the Url to load node editor script */
NodeRenderGraph.EditorURL = `${Tools._DefaultCdnUrl}/v${Engine.Version}/NodeRenderGraph/babylon.nodeRenderGraph.js`;
/** Define the Url to load snippets */
NodeRenderGraph.SnippetUrl = `https://snippet.babylonjs.com`;
__decorate([
serialize()
], NodeRenderGraph.prototype, "name", void 0);
__decorate([
serialize("comment")
], NodeRenderGraph.prototype, "comment", void 0);
//# sourceMappingURL=nodeRenderGraph.js.map