@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
437 lines (411 loc) • 14.6 kB
text/typescript
/**
* Create a soft body solver
*
*
*/
import {Object3D, Vector3} from 'three';
import {CoreGroup} from '../../../core/geometry/Group';
import {NodeParamsConfig} from '../utils/params/ParamsConfig';
import {NodeContext} from '../../poly/NodeContext';
import {InputCloneMode} from '../../poly/InputCloneMode';
import {SopType} from '../../poly/registers/nodes/types/Sop';
import {PolyScene} from '../../scene/PolyScene';
import {isBoolean, isNumberValid, isColor, isVector, isArray, isFunction} from '../../../core/Type';
import {BaseNodeType} from '../_Base';
import {Poly} from '../../Poly';
import {ThreejsCoreObject} from '../../../core/geometry/modules/three/ThreejsCoreObject';
import {DEFAULT as DEFAULT_TESSELATION_PARAMS} from '../../../core/geometry/modules/tet/utils/TesselationParamsConfig';
import {CoreSoftBodyAttribute, SoftBodyIdAttribute} from '../../../core/softBody/SoftBodyAttribute';
import {
createOrFindSoftBodyController,
softBodyControllerNodeIdFromObject,
} from '../../../core/softBody/SoftBodyControllerRegister';
import {TetSopNode} from './_BaseTet';
import {TetEmbed} from '../../../core/softBody/Common';
import {SoftBodyPersistedConfig} from '../js/code/assemblers/softBody/SoftBodyPersistedConfig';
import {AssemblerName} from '../../poly/registers/assemblers/_BaseRegister';
import {JsAssemblerSoftBody} from '../js/code/assemblers/softBody/SoftBodyAssembler';
import {JsAssemblerController} from '../js/code/Controller';
import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController';
import {JsNodeChildrenMap} from '../../poly/registers/nodes/Js';
import {Constructor, valueof} from '../../../types/GlobalTypes';
import {BaseJsNodeType} from '../js/_Base';
import {JsParamConfig} from '../js/code/utils/JsParamConfig';
import {VelocityColliderFunctionData} from '../js/code/assemblers/_Base';
import {ParamType} from '../../poly/ParamType';
import {RegisterableVariable} from '../js/code/assemblers/_BaseJsPersistedConfigUtils';
import {JsNodeFinder} from '../js/code/utils/NodeFinder';
import {TetObject} from '../../../core/geometry/modules/tet/TetObject';
import {VelocityFunction, SDFFunction} from '../../../core/softBody/SoftBody';
import {BaseSopNodeType} from './_Base';
type FunctionArg = number | boolean | Function | RegisterableVariable;
type SoftBodyVelocityEvaluatorFunction = (...args: FunctionArg[]) => VelocityFunction;
type SoftBodyColliderEvaluatorFunction = (...args: FunctionArg[]) => SDFFunction;
interface FunctionCreationArgs {
velocity: string[];
collider: string[];
}
interface FunctionEvalArgs {
args: FunctionArg[];
argsCountBeforeParams: number;
}
interface MultiFunctionEvalArgs {
velocity: FunctionEvalArgs;
collider: FunctionEvalArgs;
}
export interface MultiFunctionPartial {
velocity: SoftBodyVelocityEvaluatorFunction | undefined;
collider: SoftBodyColliderEvaluatorFunction | undefined;
}
export interface MultiFunctionDefined {
velocity: VelocityFunction;
collider: SDFFunction;
}
export interface EvalArgsWithParamConfigs {
velocity: FunctionArg[];
collider: FunctionArg[];
}
type FunctionType = 'velocity' | 'collider';
interface EvaluationGlobals {
position: Vector3;
velocity: Vector3;
time: number;
delta: number;
}
class TetSoftBodySolverSopParamsConfig extends NodeParamsConfig {}
const ParamsConfig = new TetSoftBodySolverSopParamsConfig();
export class TetSoftBodySolverSopNode extends TetSopNode<TetSoftBodySolverSopParamsConfig> {
override readonly paramsConfig = ParamsConfig;
static override type(): SopType.TET_SOFT_BODY_SOLVER {
return SopType.TET_SOFT_BODY_SOLVER;
}
private _nextId = 0;
private _tetEmbedByThreejsObjectEphemeralId: Map<number, TetEmbed> = new Map();
override readonly persisted_config: SoftBodyPersistedConfig = new SoftBodyPersistedConfig(this);
assemblerController() {
return this._assemblerController;
}
public override usedAssembler(): Readonly<AssemblerName.JS_SOFT_BODY> {
return AssemblerName.JS_SOFT_BODY;
}
protected _assemblerController = this._createAssemblerController();
private _createAssemblerController(): JsAssemblerController<JsAssemblerSoftBody> | undefined {
return Poly.assemblersRegister.assembler(this, this.usedAssembler());
}
protected override _childrenControllerContext = NodeContext.JS;
protected override initializeNode() {
this.io.inputs.setCount(1);
// set to always clone, so that we reset the solver
// by simply setting this node to dirty
this.io.inputs.initInputsClonedState(InputCloneMode.ALWAYS);
}
override createNode<S extends keyof JsNodeChildrenMap>(
node_class: S,
options?: NodeCreateOptions
): JsNodeChildrenMap[S];
override createNode<K extends valueof<JsNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K;
override createNode<K extends valueof<JsNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K {
return super.createNode(node_class, options) as K;
}
override children() {
return super.children() as BaseJsNodeType[];
}
override nodesByType<K extends keyof JsNodeChildrenMap>(type: K): JsNodeChildrenMap[K][] {
return super.nodesByType(type) as JsNodeChildrenMap[K][];
}
override childrenAllowed() {
if (this.assemblerController()) {
return super.childrenAllowed();
}
return false;
}
override sceneReadonly() {
return this.assemblerController() == null;
}
override async cook(inputCoreGroups: CoreGroup[]) {
this.compileIfRequired();
const inputTetObjects = inputCoreGroups[0].tetObjects();
if (inputTetObjects) {
const newThreejsObjects: Object3D[] = [];
for (const tetObject of inputTetObjects) {
const highResObject = await this._highResObject(tetObject);
const threejsObjectsFromTetObject = tetObject.toObject3D({
...DEFAULT_TESSELATION_PARAMS,
displayTetMesh: false,
displayOuterMesh: true,
});
if (threejsObjectsFromTetObject) {
const lowResObject = isArray(threejsObjectsFromTetObject)
? threejsObjectsFromTetObject[0]
: threejsObjectsFromTetObject;
const displayedObject = highResObject ? highResObject : lowResObject;
ThreejsCoreObject.addAttribute(
displayedObject,
SoftBodyIdAttribute.SOLVER_NODE,
this.graphNodeId()
);
const nextId = this._nextId++;
ThreejsCoreObject.addAttribute(displayedObject, SoftBodyIdAttribute.EPHEMERAL_ID, nextId);
const tetEmbed: TetEmbed = {
tetObject,
lowResObject,
highResObject,
};
this._tetEmbedByThreejsObjectEphemeralId.set(nextId, tetEmbed);
Poly.onObjectsAddRemoveHooks.assignOnAddHookHandler(displayedObject, this);
newThreejsObjects.push(displayedObject);
}
}
this.setObjects(newThreejsObjects);
} else {
this.states.error.set(`no tet objects found in input`);
this.setObjects([]);
}
}
private async _highResObject(tetObject: TetObject): Promise<Object3D | undefined> {
const highResNodeId = CoreSoftBodyAttribute.getTetEmbedHighResNodeId(tetObject);
if (highResNodeId == null) {
return;
}
const node = this.scene().graph.nodeFromId(highResNodeId) as BaseSopNodeType;
if (!node) {
return;
}
const container = await node.compute();
if (!container) {
return;
}
const threejsObjects = container.coreContent()?.threejsObjects();
if (!threejsObjects) {
return;
}
return threejsObjects[0];
}
public override updateObjectOnAdd(object: Object3D, parent: Object3D) {
//
const solverNodeId = ThreejsCoreObject.attribValue(object, SoftBodyIdAttribute.SOLVER_NODE);
if (solverNodeId != null) {
if (solverNodeId != this.graphNodeId()) {
return;
}
const ephemeralId = ThreejsCoreObject.attribValue(object, SoftBodyIdAttribute.EPHEMERAL_ID) as number;
if (ephemeralId == null) {
console.error('no ephemeralId found on object', object);
}
const tetEmbed = this._tetEmbedByThreejsObjectEphemeralId.get(ephemeralId);
if (!tetEmbed) {
console.error('no tetObject found from object', object);
return;
}
createOrFindSoftBodyController(this.scene(), this, {
tetEmbed,
threejsObjectInSceneTree: object,
});
}
}
compileIfRequired() {
if (this.assemblerController()?.compileRequired()) {
this.compile();
}
}
private _evaluationGlobals: EvaluationGlobals = {
position: new Vector3(),
velocity: new Vector3(),
time: 0,
delta: 0,
};
private _paramConfigs: JsParamConfig<ParamType>[] = [];
// private _paramConfigNames: string[] = [];
private _functionData: VelocityColliderFunctionData | undefined;
private _functionCreationArgs: FunctionCreationArgs = {
velocity: [],
collider: [],
};
private _functionEvalArgs: MultiFunctionEvalArgs = {
velocity: {args: [], argsCountBeforeParams: 0},
collider: {args: [], argsCountBeforeParams: 0},
};
private _function: MultiFunctionPartial = {
velocity: undefined,
collider: undefined,
};
private _functionArgsWithParams: EvalArgsWithParamConfigs = {
velocity: [],
collider: [],
};
updateSceneGlobals(stepsCount: number, dt: number) {
this._evaluationGlobals.time = this.scene().time() + stepsCount * dt;
this._evaluationGlobals.delta = dt; //this.scene().timeController.delta();
this._functionArgsWithParams.velocity[2] = this._evaluationGlobals.time;
this._functionArgsWithParams.collider[2] = this._evaluationGlobals.time;
this._functionArgsWithParams.velocity[3] = this._evaluationGlobals.delta;
this._functionArgsWithParams.collider[3] = this._evaluationGlobals.delta;
}
setPositionGlobals(position: Vector3) {
this._evaluationGlobals.position.copy(position);
}
setPointGlobals(position: Vector3, velocity: Vector3) {
this._evaluationGlobals.position.copy(position);
this._evaluationGlobals.velocity.copy(velocity);
}
function() {
return this._function;
}
functionData() {
return this._functionData;
}
compile() {
const assemblerController = this.assemblerController();
if (!assemblerController) {
return;
}
const outputNodes: BaseJsNodeType[] = JsNodeFinder.findOutputNodes(this);
if (outputNodes.length == 0) {
this.states.error.set('one output node is required');
return;
}
if (outputNodes.length > 1) {
this.states.error.set('only one output node allowed');
return;
}
const outputNode = outputNodes[0];
if (outputNode) {
const paramNodes = JsNodeFinder.findParamGeneratingNodes(this);
const rootNodes = outputNodes.concat(paramNodes);
assemblerController.assembler.set_root_nodes(rootNodes);
// main compilation
assemblerController.assembler.updateFunction();
// get functionData
const functionData = assemblerController.assembler.functionData();
if (!functionData) {
this.states.error.set('failed to compile ');
return;
}
this.updateFromFunctionData(functionData);
}
assemblerController.post_compile();
}
updateFromFunctionData(functionData: VelocityColliderFunctionData) {
this._functionData = functionData;
const {functionBody, variableNames, variablesByName, functionNames, functionsByName, paramConfigs} =
this._functionData;
const _createFunctionArgs = (functionBody: string, type: FunctionType) => {
const wrappedBody = `
try {
${functionBody}
} catch(e) {
_setErrorFromError(e)
return 0;
}`;
const _setErrorFromError = (e: Error) => {
this.states.error.set(e.message);
};
const variables: RegisterableVariable[] = [];
const functions: Function[] = [];
for (const variableName of variableNames) {
const variable = variablesByName[variableName];
variables.push(variable);
}
for (const functionName of functionNames) {
const _func = functionsByName[functionName];
functions.push(_func);
}
this._paramConfigs = [...paramConfigs]; //[...paramConfigs];
const paramConfigNames: string[] = paramConfigs.map((pc) => pc.uniformName());
paramConfigs.forEach((p) => p.applyToNode(this));
this._functionCreationArgs[type] = [
'position',
'velocity',
'time',
'delta',
'_setErrorFromError',
...variableNames,
...functionNames,
...paramConfigNames,
wrappedBody,
];
this._functionEvalArgs[type].args = [
this._evaluationGlobals.position,
this._evaluationGlobals.velocity,
this._evaluationGlobals.time,
this._evaluationGlobals.delta,
_setErrorFromError,
...variables,
...functions,
// paramConfigs are added dynamically during cook
];
this._functionEvalArgs[type].argsCountBeforeParams = this._functionEvalArgs[type].args.length;
try {
this._function[type] = new Function(...this._functionCreationArgs[type]) as any;
} catch (e) {
console.warn(e);
this.states.error.set('failed to compile');
}
};
_createFunctionArgs(functionBody.velocity, 'velocity');
_createFunctionArgs(functionBody.collider, 'collider');
}
functionEvalArgsWithParamConfigs() {
const _args = (type: FunctionType) => {
const argsData = this._functionEvalArgs[type];
const list = argsData.args;
let i = argsData.argsCountBeforeParams;
// const list: Array<Function | RegisterableVariable | number | boolean> = [...this._functionEvalArgs[type]];
for (const paramConfig of this._paramConfigs) {
const paramName = paramConfig.name();
const spareParam = this.params.get(paramName);
if (spareParam && spareParam.value != null) {
if (
isBoolean(spareParam.value) ||
isNumberValid(spareParam.value) ||
isColor(spareParam.value) ||
isVector(spareParam.value)
) {
list[i] = spareParam.value;
i++;
} else {
console.warn(`spareParam not found but type not yet copied to function args:'${paramName}'`);
}
} else {
console.warn(`spareParam not found:'${paramName}'`);
}
}
return list;
};
this._functionArgsWithParams.velocity = _args('velocity');
this._functionArgsWithParams.collider = _args('collider');
return this._functionArgsWithParams;
}
}
export function getSoftBodyControllerNodeFromSolverObject(
solverObject: Object3D,
scene: PolyScene
): TetSoftBodySolverSopNode | undefined {
const nodeId = softBodyControllerNodeIdFromObject(solverObject);
if (nodeId == null) {
return;
}
const graphNode = scene.graph.nodeFromId(nodeId);
if (!graphNode) {
return;
}
const node: BaseNodeType | null = isFunction((graphNode as BaseNodeType).context)
? (graphNode as BaseNodeType)
: null;
if (!node) {
return;
}
if (node.context() != NodeContext.SOP) {
return;
}
if (node.type() != SopType.TET_SOFT_BODY_SOLVER) {
return;
}
return node as TetSoftBodySolverSopNode;
}