@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
272 lines (249 loc) • 9.94 kB
text/typescript
/**
* Allows to create particle systems that will run on the GPU using gl nodes.
*
* @remarks
* TBD
*
*
*/
// SPECS:
// - simulation shaders should update the particles at any frame, and resimulate accordingly when at later frames
// - render material should update at any frame, without having to resimulate
// - changing the input will recompute, when on first frame only (otherwise an animated geo could make it recompute all the time)
import {Object3D} from 'three';
import {Constructor, valueof} from '../../../types/GlobalTypes';
import {TypedSopNode} from './_Base';
import {GlobalsTextureHandler, GlobalsTextureHandlerPurpose} from '../gl/code/globals/Texture';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {NodeContext} from '../../poly/NodeContext';
import {CoreGroup} from '../../../core/geometry/Group';
import {CoreParticlesAttribute} from '../../../core/particles/CoreParticlesAttribute';
import {
createOrFindParticlesController,
disposeParticlesFromNode,
setParticleRenderer,
} from '../../../core/particles/CoreParticles';
import {CoreParticlesController} from '../../../core/particles/CoreParticlesController';
import {PARTICLE_DATA_TYPES} from '../../../core/particles/CoreParticlesGpuComputeController';
import {GlNodeChildrenMap} from '../../poly/registers/nodes/Gl';
import {BaseGlNodeType} from '../gl/_Base';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {ShaderName} from '../utils/shaders/ShaderName';
import {GlNodeFinder} from '../gl/code/utils/NodeFinder';
import {AssemblerName} from '../../poly/registers/assemblers/_BaseRegister';
import {Poly} from '../../Poly';
import {ParticlesPersistedConfig} from '../gl/code/assemblers/particles/ParticlesPersistedConfig';
import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController';
import {SopType} from '../../poly/registers/nodes/types/Sop';
import {GlAssemblerController} from '../gl/code/Controller';
import {ShaderAssemblerParticles} from '../gl/code/assemblers/particles/Particles';
import {ParticlesSystemGpuAttributesSopOperation} from '../../operations/sop/ParticlesSystemGpuAttributes';
import {ParticlesSystemGpuMaterialSopOperation} from '../../operations/sop/ParticlesSystemGpuMaterial';
import {CoreMask} from '../../../core/geometry/Mask';
interface OperationContainer {
attributes: ParticlesSystemGpuAttributesSopOperation;
material: ParticlesSystemGpuMaterialSopOperation;
}
const DEFAULT = ParticlesSystemGpuAttributesSopOperation.DEFAULT_PARAMS;
class ParticlesSystemGpuSopParamsConfig extends NodeParamsConfig {
/** @param group to assign the material to */
group = ParamConfig.STRING(DEFAULT.group, {
objectMask: true,
});
/** @param data type used by the solver */
dataType = ParamConfig.INTEGER(0, {
menu: {
entries: PARTICLE_DATA_TYPES.map((value, index) => {
return {value: index, name: value};
}),
},
});
/** @param number of frames to run before scene plays */
preRollFramesCount = ParamConfig.INTEGER(0, {
range: [0, 100],
rangeLocked: [true, false],
});
/** @param material used to render the particles */
material = ParamConfig.NODE_PATH('', {
// separatorBefore: true,
nodeSelection: {
context: NodeContext.MAT,
},
dependentOnFoundNode: false,
separatorAfter: true,
});
}
const ParamsConfig = new ParticlesSystemGpuSopParamsConfig();
export class ParticlesSystemGpuSopNode extends TypedSopNode<ParticlesSystemGpuSopParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return SopType.PARTICLES_SYSTEM_GPU;
}
override dispose() {
disposeParticlesFromNode(this);
super.dispose();
}
assemblerController() {
return this._assemblerController;
}
public override usedAssembler(): Readonly<AssemblerName.GL_PARTICLES> {
return AssemblerName.GL_PARTICLES;
}
protected _assemblerController = this._createAssemblerController();
private _createAssemblerController(): GlAssemblerController<ShaderAssemblerParticles> | undefined {
return Poly.assemblersRegister.assembler(this, this.usedAssembler());
}
public override readonly persisted_config: ParticlesPersistedConfig = new ParticlesPersistedConfig(this);
private _particlesGlobalsHandler = new GlobalsTextureHandler(
GlobalsTextureHandler.PARTICLE_SIM_UV,
GlobalsTextureHandlerPurpose.PARTICLES_SHADER
);
private _shadersByName: Map<ShaderName, string> = new Map();
shadersByName() {
return this._shadersByName;
}
static override requireWebGL2() {
return true;
}
protected override _childrenControllerContext = NodeContext.GL;
override initializeNode() {
this.io.inputs.setCount(1);
// set to never at the moment
// otherwise the input is cloned on every frame inside cook_main()
this.io.inputs.initInputsClonedState(InputCloneMode.ALWAYS);
}
override createNode<S extends keyof GlNodeChildrenMap>(
node_class: S,
options?: NodeCreateOptions
): GlNodeChildrenMap[S];
override createNode<K extends valueof<GlNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K;
override createNode<K extends valueof<GlNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K {
return super.createNode(node_class, options) as K;
}
override children() {
return super.children() as BaseGlNodeType[];
}
override nodesByType<K extends keyof GlNodeChildrenMap>(type: K): GlNodeChildrenMap[K][] {
return super.nodesByType(type) as GlNodeChildrenMap[K][];
}
override childrenAllowed() {
if (this.assemblerController()) {
return super.childrenAllowed();
}
return false;
}
override sceneReadonly() {
return this.assemblerController() == null;
}
private _operation: OperationContainer | undefined;
override async cook(inputCoreGroups: CoreGroup[]) {
this._operation = this._operation || {
attributes: new ParticlesSystemGpuAttributesSopOperation(this._scene, this.states, this),
material: new ParticlesSystemGpuMaterialSopOperation(this._scene, this.states, this),
};
this.compileIfRequired();
const coreGroup = inputCoreGroups[0];
const selectedObjects = CoreMask.filterThreejsObjects(coreGroup, this.pv);
for (const object of selectedObjects) {
const existingActorIds = this.scene().actorsManager.objectActorNodeIds(object);
if (existingActorIds == null || existingActorIds.length == 0) {
this.states.error.set(`the input objects requires an actor node assigned to it`);
return;
}
}
const renderer = await this.scene().renderersRegister.waitForRenderer();
if (!renderer) {
this.states.error.set(`no renderer found`);
return;
}
for (const object of selectedObjects) {
Poly.onObjectsAddRemoveHooks.assignOnAddHookHandler(object, this);
setParticleRenderer(this.graphNodeId(), renderer);
CoreParticlesAttribute.setParticlesNodeId(object, this.graphNodeId());
CoreParticlesAttribute.setDataType(object, this.pv.dataType);
CoreParticlesAttribute.setPreRollFramesCount(object, this.pv.preRollFramesCount);
}
this._operation.attributes.cook(inputCoreGroups, this.pv);
await this._operation.material.cook(inputCoreGroups, this.pv);
this.setObjects(selectedObjects);
}
public override updateObjectOnAdd(object: Object3D) {
const particlesNodeId = CoreParticlesAttribute.getParticlesNodeId(object);
if (particlesNodeId == null) {
return;
}
if (particlesNodeId != this.graphNodeId()) {
return;
}
createOrFindParticlesController(object, this.scene());
}
compileIfRequired() {
if (this.assemblerController()?.compileRequired()) {
// this.debugMessage('particles:this.run_assembler() START');
try {
this.run_assembler();
} catch (err) {
const message = (err as any).message || 'failed to compile';
this.states.error.set(message);
}
// this.debugMessage('particles:this.run_assembler() END');
}
}
run_assembler() {
const assemblerController = this.assemblerController();
if (!assemblerController) {
return;
}
const exportNodes = this._findExportNodes();
if (exportNodes.length > 0) {
const rootNodes = exportNodes.concat(GlNodeFinder.findAjacencyNodes(this));
assemblerController.setAssemblerGlobalsHandler(this._particlesGlobalsHandler);
assemblerController.assembler.set_root_nodes(rootNodes);
assemblerController.assembler.compile();
assemblerController.post_compile();
}
const shadersByName = assemblerController.assembler.shaders_by_name();
this._setShaderNames(shadersByName);
}
private _setShaderNames(shadersByName: Map<ShaderName, string>) {
this._shadersByName = shadersByName;
}
init_with_persisted_config() {
const shaders_by_name = this.persisted_config.shaders_by_name();
const texture_allocations_controller = this.persisted_config.texture_allocations_controller();
if (shaders_by_name && texture_allocations_controller) {
this._setShaderNames(shaders_by_name);
// this.gpuController.setPersistedTextureAllocationController(texture_allocations_controller);
}
}
initCoreParticlesControllerFromPersistedConfig(coreParticlesController: CoreParticlesController) {
const shaders_by_name = this.persisted_config.shaders_by_name();
const texture_allocations_controller = this.persisted_config.texture_allocations_controller();
if (shaders_by_name && texture_allocations_controller) {
// this._setShaderNames(shaders_by_name);
coreParticlesController.setPersistedTextureAllocationController(texture_allocations_controller);
}
}
private _findExportNodes() {
const nodes: BaseGlNodeType[] = GlNodeFinder.findAttributeExportNodes(this);
const outputNodes = GlNodeFinder.findOutputNodes(this);
if (outputNodes.length == 0) {
this.states.error.set('one output node is required');
}
if (outputNodes.length > 1) {
this.states.error.set('only one output node is allowed');
return [];
}
const outputNode = outputNodes[0];
if (outputNode) {
nodes.push(outputNode);
}
return nodes;
}
}