@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.
1,161 lines (1,160 loc) • 91 kB
JavaScript
import { __decorate } from "../../tslib.es6.js";
import { PushMaterial } from "../pushMaterial.js";
import { Matrix, Vector2 } from "../../Maths/math.vector.js";
import { Color3, Color4 } from "../../Maths/math.color.js";
import { NodeMaterialBuildState } from "./nodeMaterialBuildState.js";
import { Effect } from "../effect.js";
import { Observable } from "../../Misc/observable.js";
import { NodeMaterialBlockTargets } from "./Enums/nodeMaterialBlockTargets.js";
import { NodeMaterialBuildStateSharedData } from "./nodeMaterialBuildStateSharedData.js";
import { MaterialDefines } from "../../Materials/materialDefines.js";
import { VertexBuffer } from "../../Buffers/buffer.js";
import { Tools } from "../../Misc/tools.js";
import { SfeModeDefine } from "./Blocks/Fragment/smartFilterFragmentOutputBlock.js";
import { TransformBlock } from "./Blocks/transformBlock.js";
import { VertexOutputBlock } from "./Blocks/Vertex/vertexOutputBlock.js";
import { FragmentOutputBlock } from "./Blocks/Fragment/fragmentOutputBlock.js";
import { InputBlock } from "./Blocks/Input/inputBlock.js";
import { GetClass, RegisterClass } from "../../Misc/typeStore.js";
import { serialize } from "../../Misc/decorators.js";
import { SerializationHelper } from "../../Misc/decorators.serialization.js";
import { CurrentScreenBlock } from "./Blocks/Dual/currentScreenBlock.js";
import { ParticleTextureBlock } from "./Blocks/Particle/particleTextureBlock.js";
import { ParticleRampGradientBlock } from "./Blocks/Particle/particleRampGradientBlock.js";
import { ParticleBlendMultiplyBlock } from "./Blocks/Particle/particleBlendMultiplyBlock.js";
import { EffectFallbacks } from "../effectFallbacks.js";
import { WebRequest } from "../../Misc/webRequest.js";
import { PostProcess } from "../../PostProcesses/postProcess.js";
import { VectorMergerBlock } from "./Blocks/vectorMergerBlock.js";
import { RemapBlock } from "./Blocks/remapBlock.js";
import { MultiplyBlock } from "./Blocks/multiplyBlock.js";
import { NodeMaterialModes } from "./Enums/nodeMaterialModes.js";
import { Texture } from "../Textures/texture.js";
import { BaseParticleSystem } from "../../Particles/baseParticleSystem.js";
import { ColorSplitterBlock } from "./Blocks/colorSplitterBlock.js";
import { TimingTools } from "../../Misc/timingTools.js";
import { ProceduralTexture } from "../Textures/Procedurals/proceduralTexture.js";
import { AnimatedInputBlockTypes } from "./Blocks/Input/animatedInputBlockTypes.js";
import { TrigonometryBlock, TrigonometryBlockOperations } from "./Blocks/trigonometryBlock.js";
import { NodeMaterialSystemValues } from "./Enums/nodeMaterialSystemValues.js";
import { EngineStore } from "../../Engines/engineStore.js";
import { Logger } from "../../Misc/logger.js";
import { PrepareDefinesForCamera, PrepareDefinesForPrePass } from "../materialHelper.functions.js";
import { AbstractEngine } from "../../Engines/abstractEngine.js";
import { MaterialHelperGeometryRendering } from "../materialHelper.geometryrendering.js";
const onCreatedEffectParameters = { effect: null, subMesh: null };
/** @internal */
export class NodeMaterialDefines extends MaterialDefines {
/**
* Creates a new NodeMaterialDefines
*/
constructor() {
super();
/** Normal */
this.NORMAL = false;
/** Tangent */
this.TANGENT = false;
/** Vertex color */
this.VERTEXCOLOR_NME = false;
/** Uv1 **/
this.UV1 = false;
/** Uv2 **/
this.UV2 = false;
/** Uv3 **/
this.UV3 = false;
/** Uv4 **/
this.UV4 = false;
/** Uv5 **/
this.UV5 = false;
/** Uv6 **/
this.UV6 = false;
/** Prepass **/
this.PREPASS = false;
/** Prepass normal */
this.PREPASS_NORMAL = false;
/** Prepass normal index */
this.PREPASS_NORMAL_INDEX = -1;
/** Prepass world normal */
this.PREPASS_WORLD_NORMAL = false;
/** Prepass world normal index */
this.PREPASS_WORLD_NORMAL_INDEX = -1;
/** Prepass position */
this.PREPASS_POSITION = false;
/** Prepass position index */
this.PREPASS_POSITION_INDEX = -1;
/** Prepass local position */
this.PREPASS_LOCAL_POSITION = false;
/** Prepass local position index */
this.PREPASS_LOCAL_POSITION_INDEX = -1;
/** Prepass depth */
this.PREPASS_DEPTH = false;
/** Prepass depth index */
this.PREPASS_DEPTH_INDEX = -1;
/** Clip-space depth */
this.PREPASS_SCREENSPACE_DEPTH = false;
/** Clip-space depth index */
this.PREPASS_SCREENSPACE_DEPTH_INDEX = -1;
/** Scene MRT count */
this.SCENE_MRT_COUNT = 0;
/** BONES */
this.NUM_BONE_INFLUENCERS = 0;
/** Bones per mesh */
this.BonesPerMesh = 0;
/** Using texture for bone storage */
this.BONETEXTURE = false;
/** MORPH TARGETS */
this.MORPHTARGETS = false;
/** Morph target position */
this.MORPHTARGETS_POSITION = false;
/** Morph target normal */
this.MORPHTARGETS_NORMAL = false;
/** Morph target tangent */
this.MORPHTARGETS_TANGENT = false;
/** Morph target uv */
this.MORPHTARGETS_UV = false;
/** Morph target uv2 */
this.MORPHTARGETS_UV2 = false;
this.MORPHTARGETS_COLOR = false;
/** Morph target support positions */
this.MORPHTARGETTEXTURE_HASPOSITIONS = false;
/** Morph target support normals */
this.MORPHTARGETTEXTURE_HASNORMALS = false;
/** Morph target support tangents */
this.MORPHTARGETTEXTURE_HASTANGENTS = false;
/** Morph target support uvs */
this.MORPHTARGETTEXTURE_HASUVS = false;
/** Morph target support uv2s */
this.MORPHTARGETTEXTURE_HASUV2S = false;
this.MORPHTARGETTEXTURE_HASCOLORS = false;
/** Number of morph influencers */
this.NUM_MORPH_INFLUENCERS = 0;
/** Using a texture to store morph target data */
this.MORPHTARGETS_TEXTURE = false;
/** IMAGE PROCESSING */
this.IMAGEPROCESSING = false;
/** Vignette */
this.VIGNETTE = false;
/** Multiply blend mode for vignette */
this.VIGNETTEBLENDMODEMULTIPLY = false;
/** Opaque blend mode for vignette */
this.VIGNETTEBLENDMODEOPAQUE = false;
/** Tone mapping */
this.TONEMAPPING = 0;
/** Contrast */
this.CONTRAST = false;
/** Exposure */
this.EXPOSURE = false;
/** Color curves */
this.COLORCURVES = false;
/** Color grading */
this.COLORGRADING = false;
/** 3D color grading */
this.COLORGRADING3D = false;
/** Sampler green depth */
this.SAMPLER3DGREENDEPTH = false;
/** Sampler for BGR map */
this.SAMPLER3DBGRMAP = false;
/** Dithering */
this.DITHER = false;
/** Using post process for image processing */
this.IMAGEPROCESSINGPOSTPROCESS = false;
/** Skip color clamp */
this.SKIPFINALCOLORCLAMP = false;
/** MISC. */
this.BUMPDIRECTUV = 0;
/** Camera is orthographic */
this.CAMERA_ORTHOGRAPHIC = false;
/** Camera is perspective */
this.CAMERA_PERSPECTIVE = false;
this.AREALIGHTSUPPORTED = true;
this.AREALIGHTNOROUGHTNESS = true;
this.rebuild();
}
/**
* Set the value of a specific key
* @param name defines the name of the key to set
* @param value defines the value to set
* @param markAsUnprocessedIfDirty Flag to indicate to the cache that this value needs processing
*/
setValue(name, value, markAsUnprocessedIfDirty = false) {
if (this[name] === undefined) {
this._keys.push(name);
}
if (markAsUnprocessedIfDirty && this[name] !== value) {
this.markAsUnprocessed();
}
this[name] = value;
}
}
/**
* Class used to create a node based material built by assembling shader blocks
*/
export class NodeMaterial extends PushMaterial {
/**
* Checks if a block is a texture block
* @param block The block to check
* @returns True if the block is a texture block
*/
static _BlockIsTextureBlock(block) {
return (block.getClassName() === "TextureBlock" ||
block.getClassName() === "ReflectionTextureBaseBlock" ||
block.getClassName() === "ReflectionTextureBlock" ||
block.getClassName() === "ReflectionBlock" ||
block.getClassName() === "RefractionBlock" ||
block.getClassName() === "CurrentScreenBlock" ||
block.getClassName() === "SmartFilterTextureBlock" ||
block.getClassName() === "ParticleTextureBlock" ||
block.getClassName() === "ImageSourceBlock" ||
block.getClassName() === "TriPlanarBlock" ||
block.getClassName() === "BiPlanarBlock" ||
block.getClassName() === "PrePassTextureBlock");
}
set _glowModeEnabled(value) {
this._useAdditionalColor = value;
}
/** Get the inspector from bundle or global
* @returns the global NME
*/
_getGlobalNodeMaterialEditor() {
// UMD Global name detection from Webpack Bundle UMD Name.
if (typeof NODEEDITOR !== "undefined") {
return NODEEDITOR;
}
// In case of module let's check the global emitted from the editor entry point.
if (typeof BABYLON !== "undefined" && typeof BABYLON.NodeEditor !== "undefined") {
return BABYLON;
}
return undefined;
}
/** Gets or sets the active shader language */
get shaderLanguage() {
return this._options?.shaderLanguage || NodeMaterial.DefaultShaderLanguage;
}
set shaderLanguage(value) {
this._options.shaderLanguage = value;
}
/** Gets or sets options to control the node material overall behavior */
get options() {
return this._options;
}
set options(options) {
this._options = options;
}
/**
* Gets the image processing configuration used either in this material.
*/
get imageProcessingConfiguration() {
return this._imageProcessingConfiguration;
}
/**
* Sets the Default image processing configuration used either in the this material.
*
* If sets to null, the scene one is in use.
*/
set imageProcessingConfiguration(value) {
this._attachImageProcessingConfiguration(value);
// Ensure the effect will be rebuilt.
this._markAllSubMeshesAsTexturesDirty();
}
/**
* Gets or sets the mode property
*/
get mode() {
return this._mode;
}
set mode(value) {
this._mode = value;
}
/** Gets or sets the unique identifier used to identified the effect associated with the material */
get buildId() {
return this._buildId;
}
set buildId(value) {
this._buildId = value;
}
/**
* Create a new node based material
* @param name defines the material name
* @param scene defines the hosting scene
* @param options defines creation option
*/
constructor(name, scene, options = {}) {
super(name, scene || EngineStore.LastCreatedScene);
this._buildId = NodeMaterial._BuildIdGenerator++;
this._buildWasSuccessful = false;
this._cachedWorldViewMatrix = new Matrix();
this._cachedWorldViewProjectionMatrix = new Matrix();
this._optimizers = new Array();
this._animationFrame = -1;
this._buildIsInProgress = false;
this.BJSNODEMATERIALEDITOR = this._getGlobalNodeMaterialEditor();
/** @internal */
this._useAdditionalColor = false;
/**
* Gets or sets data used by visual editor
* @see https://nme.babylonjs.com
*/
this.editorData = null;
/**
* Gets or sets a boolean indicating that alpha value must be ignored (This will turn alpha blending off even if an alpha value is produced by the material)
*/
this.ignoreAlpha = false;
/**
* Defines the maximum number of lights that can be used in the material
*/
this.maxSimultaneousLights = 4;
/**
* Observable raised when the material is built
*/
this.onBuildObservable = new Observable();
/**
* Observable raised when an error is detected
*/
this.onBuildErrorObservable = new Observable();
/**
* Gets or sets the root nodes of the material vertex shader
*/
this._vertexOutputNodes = new Array();
/**
* Gets or sets the root nodes of the material fragment (pixel) shader
*/
this._fragmentOutputNodes = new Array();
/**
* Gets an array of blocks that needs to be serialized even if they are not yet connected
*/
this.attachedBlocks = [];
/**
* Specifies the mode of the node material
* @internal
*/
this._mode = NodeMaterialModes.Material;
/**
* Gets or sets a boolean indicating that alpha blending must be enabled no matter what alpha value or alpha channel of the FragmentBlock are
*/
this.forceAlphaBlending = false;
if (!NodeMaterial.UseNativeShaderLanguageOfEngine && options && options.shaderLanguage === 1 /* ShaderLanguage.WGSL */ && !this.getScene().getEngine().isWebGPU) {
throw new Error("WebGPU shader language is only supported with WebGPU engine");
}
this._options = {
emitComments: false,
shaderLanguage: NodeMaterial.DefaultShaderLanguage,
...options,
};
if (NodeMaterial.UseNativeShaderLanguageOfEngine) {
this._options.shaderLanguage = this.getScene().getEngine().isWebGPU ? 1 /* ShaderLanguage.WGSL */ : 0 /* ShaderLanguage.GLSL */;
}
// Setup the default processing configuration to the scene.
this._attachImageProcessingConfiguration(null);
}
/**
* Gets the current class name of the material e.g. "NodeMaterial"
* @returns the class name
*/
getClassName() {
return "NodeMaterial";
}
/**
* Attaches a new image processing configuration to the Standard Material.
* @param configuration
*/
_attachImageProcessingConfiguration(configuration) {
if (configuration === this._imageProcessingConfiguration) {
return;
}
// Detaches observer.
if (this._imageProcessingConfiguration && this._imageProcessingObserver) {
this._imageProcessingConfiguration.onUpdateParameters.remove(this._imageProcessingObserver);
}
// Pick the scene configuration if needed.
if (!configuration) {
this._imageProcessingConfiguration = this.getScene().imageProcessingConfiguration;
}
else {
this._imageProcessingConfiguration = configuration;
}
// Attaches observer.
if (this._imageProcessingConfiguration) {
this._imageProcessingObserver = this._imageProcessingConfiguration.onUpdateParameters.add(() => {
this._markAllSubMeshesAsImageProcessingDirty();
});
}
}
/**
* Get 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 an input block using a predicate
* @param predicate defines the predicate used to find the good candidate
* @returns the required input block or null if not found
*/
getInputBlockByPredicate(predicate) {
for (const block of this.attachedBlocks) {
if (block.isInput && predicate(block)) {
return block;
}
}
return null;
}
/**
* 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;
}
/**
* Adds a new optimizer to the list of optimizers
* @param optimizer defines the optimizers to add
* @returns the current material
*/
registerOptimizer(optimizer) {
const index = this._optimizers.indexOf(optimizer);
if (index > -1) {
return;
}
this._optimizers.push(optimizer);
return this;
}
/**
* Remove an optimizer from the list of optimizers
* @param optimizer defines the optimizers to remove
* @returns the current material
*/
unregisterOptimizer(optimizer) {
const index = this._optimizers.indexOf(optimizer);
if (index === -1) {
return;
}
this._optimizers.splice(index, 1);
return this;
}
/**
* Add a new block to the list of output nodes
* @param node defines the node to add
* @returns the current material
*/
addOutputNode(node) {
if (node.target === null) {
// eslint-disable-next-line no-throw-literal
throw "This node is not meant to be an output node. You may want to explicitly set its target value.";
}
if ((node.target & NodeMaterialBlockTargets.Vertex) !== 0) {
this._addVertexOutputNode(node);
}
if ((node.target & NodeMaterialBlockTargets.Fragment) !== 0) {
this._addFragmentOutputNode(node);
}
return this;
}
/**
* Remove a block from the list of root nodes
* @param node defines the node to remove
* @returns the current material
*/
removeOutputNode(node) {
if (node.target === null) {
return this;
}
if ((node.target & NodeMaterialBlockTargets.Vertex) !== 0) {
this._removeVertexOutputNode(node);
}
if ((node.target & NodeMaterialBlockTargets.Fragment) !== 0) {
this._removeFragmentOutputNode(node);
}
return this;
}
_addVertexOutputNode(node) {
if (this._vertexOutputNodes.indexOf(node) !== -1) {
return;
}
node.target = NodeMaterialBlockTargets.Vertex;
this._vertexOutputNodes.push(node);
return this;
}
_removeVertexOutputNode(node) {
const index = this._vertexOutputNodes.indexOf(node);
if (index === -1) {
return;
}
this._vertexOutputNodes.splice(index, 1);
return this;
}
_addFragmentOutputNode(node) {
if (this._fragmentOutputNodes.indexOf(node) !== -1) {
return;
}
node.target = NodeMaterialBlockTargets.Fragment;
this._fragmentOutputNodes.push(node);
return this;
}
_removeFragmentOutputNode(node) {
const index = this._fragmentOutputNodes.indexOf(node);
if (index === -1) {
return;
}
this._fragmentOutputNodes.splice(index, 1);
return this;
}
get _supportGlowLayer() {
if (this._fragmentOutputNodes.length === 0) {
return false;
}
if (this._fragmentOutputNodes.some((f) => f.additionalColor && f.additionalColor.isConnected)) {
return true;
}
return false;
}
/**
* Specifies if the material will require alpha blending
* @returns a boolean specifying if alpha blending is needed
*/
needAlphaBlending() {
if (this.ignoreAlpha) {
return false;
}
return this.forceAlphaBlending || this.alpha < 1.0 || (this._sharedData && this._sharedData.hints.needAlphaBlending);
}
/**
* Specifies if this material should be rendered in alpha test mode
* @returns a boolean specifying if an alpha test is needed.
*/
needAlphaTesting() {
return this._sharedData && this._sharedData.hints.needAlphaTesting;
}
_processInitializeOnLink(block, state, nodesToProcessForOtherBuildState, autoConfigure = true) {
if (block.target === NodeMaterialBlockTargets.VertexAndFragment) {
nodesToProcessForOtherBuildState.push(block);
}
else if (state.target === NodeMaterialBlockTargets.Fragment && block.target === NodeMaterialBlockTargets.Vertex && block._preparationId !== this._buildId) {
nodesToProcessForOtherBuildState.push(block);
}
this._initializeBlock(block, state, nodesToProcessForOtherBuildState, autoConfigure);
}
_attachBlock(node) {
if (this.attachedBlocks.indexOf(node) === -1) {
if (node.isUnique) {
const className = node.getClassName();
for (const other of this.attachedBlocks) {
if (other.getClassName() === className) {
this._sharedData.raiseBuildError(`Cannot have multiple blocks of type ${className} in the same NodeMaterial`);
return;
}
}
}
this.attachedBlocks.push(node);
}
}
_initializeBlock(node, state, nodesToProcessForOtherBuildState, autoConfigure = true) {
node.initialize(state);
if (autoConfigure) {
node.autoConfigure(this);
}
node._preparationId = this._buildId;
this._attachBlock(node);
for (const input of node.inputs) {
input.associatedVariableName = "";
const connectedPoint = input.connectedPoint;
if (connectedPoint && !connectedPoint._preventBubbleUp) {
const block = connectedPoint.ownerBlock;
if (block !== node) {
this._processInitializeOnLink(block, state, nodesToProcessForOtherBuildState, autoConfigure);
}
}
}
// Loop
if (node.isLoop) {
// We need to keep the storage write block in the active blocks
const loopBlock = node;
if (loopBlock.loopID.hasEndpoints) {
for (const endpoint of loopBlock.loopID.endpoints) {
const block = endpoint.ownerBlock;
if (block.outputs.length !== 0) {
continue;
}
state._terminalBlocks.add(block); // Attach the storage write only
this._processInitializeOnLink(block, state, nodesToProcessForOtherBuildState, autoConfigure);
}
}
}
else if (node.isTeleportOut) {
// Teleportation
const teleport = node;
if (teleport.entryPoint) {
this._processInitializeOnLink(teleport.entryPoint, state, nodesToProcessForOtherBuildState, autoConfigure);
}
}
for (const output of node.outputs) {
output.associatedVariableName = "";
}
}
_resetDualBlocks(node, id) {
if (node.target === NodeMaterialBlockTargets.VertexAndFragment) {
node.buildId = id;
}
for (const input of node.inputs) {
const connectedPoint = input.connectedPoint;
if (connectedPoint && !connectedPoint._preventBubbleUp) {
const block = connectedPoint.ownerBlock;
if (block !== node) {
this._resetDualBlocks(block, id);
}
}
}
// If this is a teleport out, we need to reset the connected block
if (node.isTeleportOut) {
const teleportOut = node;
if (teleportOut.entryPoint) {
this._resetDualBlocks(teleportOut.entryPoint, id);
}
}
else if (node.isLoop) {
// Loop
const loopBlock = node;
if (loopBlock.loopID.hasEndpoints) {
for (const endpoint of loopBlock.loopID.endpoints) {
const block = endpoint.ownerBlock;
if (block.outputs.length !== 0) {
continue;
}
this._resetDualBlocks(block, id);
}
}
}
}
/**
* Remove a block from the current node material
* @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.isFinalMerger) {
this.removeOutputNode(block);
}
}
/**
* Build the material and generates the inner effect
* @param verbose defines if the build should log activity
* @param updateBuildId defines if the internal build Id should be updated (default is true)
* @param autoConfigure defines if the autoConfigure method should be called when initializing blocks (default is false)
*/
build(verbose = false, updateBuildId = true, autoConfigure = false) {
if (this._buildIsInProgress) {
Logger.Warn("Build is already in progress, You can use NodeMaterial.onBuildObservable to determine when the build is completed.");
return;
}
this._buildIsInProgress = true;
// First time?
if (!this._vertexCompilationState && !autoConfigure) {
autoConfigure = true;
}
this._buildWasSuccessful = false;
const engine = this.getScene().getEngine();
const allowEmptyVertexProgram = this._mode === NodeMaterialModes.Particle || this._mode === NodeMaterialModes.SFE;
if (this._vertexOutputNodes.length === 0 && !allowEmptyVertexProgram) {
this.onBuildErrorObservable.notifyObservers("You must define at least one vertexOutputNode");
this._buildIsInProgress = false;
return;
}
if (this._fragmentOutputNodes.length === 0) {
this.onBuildErrorObservable.notifyObservers("You must define at least one fragmentOutputNode");
this._buildIsInProgress = false;
return;
}
// Compilation state
this._vertexCompilationState = new NodeMaterialBuildState();
this._vertexCompilationState.supportUniformBuffers = engine.supportsUniformBuffers;
this._vertexCompilationState.target = NodeMaterialBlockTargets.Vertex;
this._fragmentCompilationState = new NodeMaterialBuildState();
this._fragmentCompilationState.supportUniformBuffers = engine.supportsUniformBuffers;
this._fragmentCompilationState.target = NodeMaterialBlockTargets.Fragment;
// Shared data
const needToPurgeList = this._fragmentOutputNodes.filter((n) => n._isFinalOutputAndActive).length > 1;
let fragmentOutputNodes = this._fragmentOutputNodes;
if (needToPurgeList) {
// Get all but the final output nodes
fragmentOutputNodes = this._fragmentOutputNodes.filter((n) => !n._isFinalOutputAndActive);
// Get the first with precedence on
fragmentOutputNodes.push(this._fragmentOutputNodes.filter((n) => n._isFinalOutputAndActive && n._hasPrecedence)[0]);
}
this._sharedData = new NodeMaterialBuildStateSharedData();
this._sharedData.nodeMaterial = this;
this._sharedData.fragmentOutputNodes = fragmentOutputNodes;
this._vertexCompilationState.sharedData = this._sharedData;
this._fragmentCompilationState.sharedData = this._sharedData;
this._sharedData.buildId = this._buildId;
this._sharedData.emitComments = this._options.emitComments;
this._sharedData.verbose = verbose;
this._sharedData.scene = this.getScene();
this._sharedData.allowEmptyVertexProgram = allowEmptyVertexProgram;
// Initialize blocks
const vertexNodes = [];
const fragmentNodes = [];
for (const vertexOutputNode of this._vertexOutputNodes) {
vertexNodes.push(vertexOutputNode);
this._initializeBlock(vertexOutputNode, this._vertexCompilationState, fragmentNodes, autoConfigure);
}
for (const fragmentOutputNode of fragmentOutputNodes) {
fragmentNodes.push(fragmentOutputNode);
this._initializeBlock(fragmentOutputNode, this._fragmentCompilationState, vertexNodes, autoConfigure);
}
// Are blocks code ready?
let waitingNodeCount = 0;
for (const node of this.attachedBlocks) {
if (!node.codeIsReady) {
waitingNodeCount++;
node.onCodeIsReadyObservable.addOnce(() => {
waitingNodeCount--;
if (waitingNodeCount === 0) {
this._finishBuildProcess(verbose, updateBuildId, vertexNodes, fragmentNodes);
}
});
}
}
if (waitingNodeCount !== 0) {
return;
}
this._finishBuildProcess(verbose, updateBuildId, vertexNodes, fragmentNodes);
}
_finishBuildProcess(verbose = false, updateBuildId = true, vertexNodes, fragmentNodes) {
// Optimize
this.optimize();
// Vertex
for (const vertexOutputNode of vertexNodes) {
vertexOutputNode.build(this._vertexCompilationState, vertexNodes);
}
// Fragment
this._fragmentCompilationState.uniforms = this._vertexCompilationState.uniforms.slice(0);
this._fragmentCompilationState._uniformDeclaration = this._vertexCompilationState._uniformDeclaration;
this._fragmentCompilationState._constantDeclaration = this._vertexCompilationState._constantDeclaration;
this._fragmentCompilationState._vertexState = this._vertexCompilationState;
for (const fragmentOutputNode of fragmentNodes) {
this._resetDualBlocks(fragmentOutputNode, this._buildId - 1);
}
for (const fragmentOutputNode of fragmentNodes) {
fragmentOutputNode.build(this._fragmentCompilationState, fragmentNodes);
}
// Finalize
this._vertexCompilationState.finalize(this._vertexCompilationState);
this._fragmentCompilationState.finalize(this._fragmentCompilationState);
if (updateBuildId) {
this._buildId = NodeMaterial._BuildIdGenerator++;
}
if (verbose) {
Logger.Log("Vertex shader:");
Logger.Log(this._vertexCompilationState.compilationString);
Logger.Log("Fragment shader:");
Logger.Log(this._fragmentCompilationState.compilationString);
}
// Errors
const noError = this._sharedData.emitErrors();
this._buildIsInProgress = false;
if (noError) {
this._buildWasSuccessful = true;
this.onBuildObservable.notifyObservers(this);
}
// Wipe defines
const meshes = this.getScene().meshes;
for (const mesh of meshes) {
if (!mesh.subMeshes) {
continue;
}
for (const subMesh of mesh.subMeshes) {
if (subMesh.getMaterial() !== this) {
continue;
}
if (!subMesh.materialDefines) {
continue;
}
const defines = subMesh.materialDefines;
defines.markAllAsDirty();
defines.reset();
}
}
if (this.prePassTextureInputs.length) {
this.getScene().enablePrePassRenderer();
}
const prePassRenderer = this.getScene().prePassRenderer;
if (prePassRenderer) {
prePassRenderer.markAsDirty();
}
}
/**
* Runs an optimization phase to try to improve the shader code
*/
optimize() {
for (const optimizer of this._optimizers) {
optimizer.optimize(this._vertexOutputNodes, this._fragmentOutputNodes);
}
}
_prepareDefinesForAttributes(mesh, defines) {
const oldNormal = defines["NORMAL"];
const oldTangent = defines["TANGENT"];
const oldColor = defines["VERTEXCOLOR_NME"];
defines["NORMAL"] = mesh.isVerticesDataPresent(VertexBuffer.NormalKind);
defines["TANGENT"] = mesh.isVerticesDataPresent(VertexBuffer.TangentKind);
const hasVertexColors = mesh.useVertexColors && mesh.isVerticesDataPresent(VertexBuffer.ColorKind);
defines["VERTEXCOLOR_NME"] = hasVertexColors;
let uvChanged = false;
for (let i = 1; i <= 6; ++i) {
const oldUV = defines["UV" + i];
defines["UV" + i] = mesh.isVerticesDataPresent(`uv${i === 1 ? "" : i}`);
uvChanged = uvChanged || defines["UV" + i] !== oldUV;
}
// PrePass
const oit = this.needAlphaBlendingForMesh(mesh) && this.getScene().useOrderIndependentTransparency;
PrepareDefinesForPrePass(this.getScene(), defines, !oit);
MaterialHelperGeometryRendering.PrepareDefines(this.getScene().getEngine().currentRenderPassId, mesh, defines);
if (oldNormal !== defines["NORMAL"] || oldTangent !== defines["TANGENT"] || oldColor !== defines["VERTEXCOLOR_NME"] || uvChanged) {
defines.markAsAttributesDirty();
}
}
/**
* Can this material render to prepass
*/
get isPrePassCapable() {
return true;
}
/**
* Outputs written to the prepass
*/
get prePassTextureOutputs() {
const prePassOutputBlock = this.getBlockByPredicate((block) => block.getClassName() === "PrePassOutputBlock");
const result = [4];
if (!prePassOutputBlock) {
return result;
}
// Cannot write to prepass if we alread read from prepass
if (this.prePassTextureInputs.length) {
return result;
}
if (prePassOutputBlock.viewDepth.isConnected) {
result.push(5);
}
if (prePassOutputBlock.screenDepth.isConnected) {
result.push(10);
}
if (prePassOutputBlock.viewNormal.isConnected) {
result.push(6);
}
if (prePassOutputBlock.worldNormal.isConnected) {
result.push(8);
}
if (prePassOutputBlock.worldPosition.isConnected) {
result.push(1);
}
if (prePassOutputBlock.localPosition.isConnected) {
result.push(9);
}
if (prePassOutputBlock.reflectivity.isConnected) {
result.push(3);
}
if (prePassOutputBlock.velocity.isConnected) {
result.push(2);
}
if (prePassOutputBlock.velocityLinear.isConnected) {
result.push(11);
}
return result;
}
/**
* Gets the list of prepass texture required
*/
get prePassTextureInputs() {
const prePassTextureBlocks = this.getAllTextureBlocks().filter((block) => block.getClassName() === "PrePassTextureBlock");
const result = [];
for (const block of prePassTextureBlocks) {
if (block.position.isConnected && !result.includes(1)) {
result.push(1);
}
if (block.localPosition.isConnected && !result.includes(9)) {
result.push(9);
}
if (block.depth.isConnected && !result.includes(5)) {
result.push(5);
}
if (block.screenDepth.isConnected && !result.includes(10)) {
result.push(10);
}
if (block.normal.isConnected && !result.includes(6)) {
result.push(6);
}
if (block.worldNormal.isConnected && !result.includes(8)) {
result.push(8);
}
}
return result;
}
/**
* Sets the required values to the prepass renderer.
* @param prePassRenderer defines the prepass renderer to set
* @returns true if the pre pass is needed
*/
setPrePassRenderer(prePassRenderer) {
const prePassTexturesRequired = this.prePassTextureInputs.concat(this.prePassTextureOutputs);
if (prePassRenderer && prePassTexturesRequired.length > 1) {
let cfg = prePassRenderer.getEffectConfiguration("nodeMaterial");
if (!cfg) {
cfg = prePassRenderer.addEffectConfiguration({
enabled: true,
needsImageProcessing: false,
name: "nodeMaterial",
texturesRequired: [],
});
}
for (const prePassTexture of prePassTexturesRequired) {
if (!cfg.texturesRequired.includes(prePassTexture)) {
cfg.texturesRequired.push(prePassTexture);
}
}
cfg.enabled = true;
}
// COLOR_TEXTURE is always required for prepass, length > 1 means
// we actually need to write to special prepass textures
return prePassTexturesRequired.length > 1;
}
/**
* Create a post process from the material
* @param camera The camera to apply the render pass to.
* @param options The required width/height ratio to downsize to before computing the render pass. (Use 1.0 for full size)
* @param samplingMode The sampling mode to be used when computing the pass. (default: 0)
* @param engine The engine which the post process will be applied. (default: current engine)
* @param reusable If the post process can be reused on the same frame. (default: false)
* @param textureType Type of textures used when performing the post process. (default: 0)
* @param textureFormat Format of textures used when performing the post process. (default: TEXTUREFORMAT_RGBA)
* @returns the post process created
*/
createPostProcess(camera, options = 1, samplingMode = 1, engine, reusable, textureType = 0, textureFormat = 5) {
if (this.mode !== NodeMaterialModes.PostProcess && this.mode !== NodeMaterialModes.SFE) {
Logger.Log("Incompatible material mode");
return null;
}
return this._createEffectForPostProcess(null, camera, options, samplingMode, engine, reusable, textureType, textureFormat);
}
/**
* Create the post process effect from the material
* @param postProcess The post process to create the effect for
*/
createEffectForPostProcess(postProcess) {
this._createEffectForPostProcess(postProcess);
}
_createEffectForPostProcess(postProcess, camera, options = 1, samplingMode = 1, engine, reusable, textureType = 0, textureFormat = 5) {
let tempName = this.name + this._buildId;
const defines = new NodeMaterialDefines();
let buildId = this._buildId;
this._processDefines(defines);
// If no vertex shader emitted, fallback to default postprocess vertex shader
const vertexCode = this._sharedData.checks.emitVertex ? this._vertexCompilationState._builtCompilationString : undefined;
Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, vertexCode, this.shaderLanguage);
if (!postProcess) {
postProcess = new PostProcess(this.name + "PostProcess", tempName, this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, options, camera, samplingMode, engine, reusable, defines.toString(), textureType, vertexCode ? tempName : "postprocess", { maxSimultaneousLights: this.maxSimultaneousLights }, false, textureFormat, this.shaderLanguage);
}
else {
postProcess.updateEffect(defines.toString(), this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, { maxSimultaneousLights: this.maxSimultaneousLights }, undefined, undefined, tempName, tempName);
}
postProcess.nodeMaterialSource = this;
postProcess.onApplyObservable.add((effect) => {
if (buildId !== this._buildId) {
delete Effect.ShadersStore[tempName + "VertexShader"];
delete Effect.ShadersStore[tempName + "PixelShader"];
tempName = this.name + this._buildId;
defines.markAllAsDirty();
buildId = this._buildId;
}
const result = this._processDefines(defines);
if (result) {
Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, this._vertexCompilationState._builtCompilationString);
TimingTools.SetImmediate(() => postProcess.updateEffect(defines.toString(), this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, { maxSimultaneousLights: this.maxSimultaneousLights }, undefined, undefined, tempName, tempName));
}
this._checkInternals(effect);
});
return postProcess;
}
/**
* Create a new procedural texture based on this node material
* @param size defines the size of the texture
* @param scene defines the hosting scene
* @returns the new procedural texture attached to this node material
*/
createProceduralTexture(size, scene) {
if (this.mode !== NodeMaterialModes.ProceduralTexture) {
Logger.Log("Incompatible material mode");
return null;
}
let tempName = this.name + this._buildId;
const proceduralTexture = new ProceduralTexture(tempName, size, null, scene);
const defines = new NodeMaterialDefines();
const result = this._processDefines(defines);
Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, this._vertexCompilationState._builtCompilationString, this.shaderLanguage);
let effect = this.getScene().getEngine().createEffect({
vertexElement: tempName,
fragmentElement: tempName,
}, [VertexBuffer.PositionKind], this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, defines.toString(), result?.fallbacks, undefined, undefined, undefined, this.shaderLanguage);
proceduralTexture.nodeMaterialSource = this;
proceduralTexture._setEffect(effect);
let buildId = this._buildId;
const refreshEffect = () => {
if (buildId !== this._buildId) {
delete Effect.ShadersStore[tempName + "VertexShader"];
delete Effect.ShadersStore[tempName + "PixelShader"];
tempName = this.name + this._buildId;
defines.markAllAsDirty();
buildId = this._buildId;
}
const result = this._processDefines(defines);
if (result) {
Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, this._vertexCompilationState._builtCompilationString, this.shaderLanguage);
TimingTools.SetImmediate(() => {
effect = this.getScene().getEngine().createEffect({
vertexElement: tempName,
fragmentElement: tempName,
}, [VertexBuffer.PositionKind], this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, defines.toString(), result?.fallbacks, undefined);
proceduralTexture._setEffect(effect);
});
}
this._checkInternals(effect);
};
proceduralTexture.onBeforeGenerationObservable.add(() => {
refreshEffect();
});
// This is needed if the procedural texture is not set to refresh automatically
this.onBuildObservable.add(() => {
refreshEffect();
});
return proceduralTexture;
}
_createEffectForParticles(particleSystem, blendMode, onCompiled, onError, effect, defines, particleSystemDefinesJoined = "") {
let tempName = this.name + this._buildId + "_" + blendMode;
if (!defines) {
defines = new NodeMaterialDefines();
}
let buildId = this._buildId;
const particleSystemDefines = [];
let join = particleSystemDefinesJoined;
if (!effect) {
const result = this._processDefines(defines);
Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, undefined, this.shaderLanguage);
particleSystem.fillDefines(particleSystemDefines, blendMode, false);
join = particleSystemDefines.join("\n");
effect = this.getScene()
.getEngine()
.createEffectForParticles(tempName, this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, defines.toString() + "\n" + join, result?.fallbacks, onCompiled, onError, particleSystem, this.shaderLanguage);
particleSystem.setCustomEffect(effect, blendMode);
}
effect.onBindObservable.add((effect) => {
if (buildId !== this._buildId) {
delete Effect.ShadersStore[tempName + "PixelShader"];
tempName = this.name + this._buildId + "_" + blendMode;
defines.markAllAsDirty();
buildId = this._buildId;
}
particleSystemDefines.length = 0;
particleSystem.fillDefines(particleSystemDefines, blendMode, false);
const particleSystemDefinesJoinedCurrent = particleSystemDefines.join("\n");
if (particleSystemDefinesJoinedCurrent !== join) {
defines.markAllAsDirty();
join = particleSystemDefinesJoinedCurrent;
}
const result = this._processDefines(defines);
if (result) {
Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, undefined, this.shaderLanguage);
effect = this.getScene()
.getEngine()
.createEffectForParticles(tempName, this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, defines.toString() + "\n" + join, result?.fallbacks, onCompiled, onError, particleSystem);
particleSystem.setCustomEffect(effect, blendMode);
this._createEffectForParticles(particleSystem, blendMode, onCompiled, onError, effect, defines, particleSystemDefinesJoined); // add the effect.onBindObservable observer
return;
}
this._checkInternals(effect);
});
}
_checkInternals(effect) {
// Animated blocks
if (this._sharedData.animatedInputs) {
const scene = this.getScene();
const frameId = scene.getFrameId();
if (this._animationFrame !== frameId) {
for (const input of this._sharedData.animatedInputs) {
input.animate(scene);
}
this._animationFrame = frameId;
}
}
// Bindable blocks
for (const block of this._sharedData.bindableBlocks) {
block.bind(effect, this);
}
// Connection points
for (const inputBlock of this._sharedData.inputBlocks) {
inputBlock._transmit(effect, this.getScene(), this);
}
}
/**
* Create the effect to be used as the custom effect for a particle system
* @param particleSystem Particle system to create the effect for
* @param onCompiled defines a function to call when the effect creation is successful
* @param onError defines a function to call when the effect creation has failed
*/
createEffectForParticles(particleSystem, onCompiled, onError) {
if (this.mode !== NodeMaterialModes.Particle) {
Logger.Log("Incompatible material mode");
return;
}
this._createEffectForParticles(particleSystem, BaseParticleSystem.BLENDMODE_ONEONE, onCompiled, onError);
this._createEffectForParticles(particleSystem, BaseParticleSystem.BLENDMODE_MULTIPLY, onCompiled, onError);
}
/**
* Use this material as the shadow depth wrapper of a target material
* @param targetMaterial defines the target material
*/
createAsShadowDepthWrapper(targetMaterial) {
if (this.mode !== NodeMaterialModes.Material) {
Logger.Log("Incompatible material mode");
return;
}
targetMaterial.shadowDepthWrapper = new BABYLON.ShadowDepthWrapper(this, this.getScene());
}
_processDefines(defines, mesh, useInstances = false, subMesh) {
let result = null;
// Global defines
const scene = this.getScene();
if (PrepareDefinesForCamera(scene, defines)) {
defines.markAsMiscDirty()