notes/Projects/bachelor/RUNTIME_EXECUTOR.md

233 lines
7.6 KiB
Markdown
Raw Normal View History

2024-03-01 13:37:14 +01:00
# RUNTIME_EXECUTOR
2024-03-20 17:22:37 +01:00
```toc
```
2024-03-01 13:37:14 +01:00
This component receives a serialized node_graph and returns the output. I generally love the idea that a node is a pure function which just receives its own inputs and then calculates them. BUT! there is always a but, there are some things that the nodes needs that are not its own inputs. These are in my experience:
**run_seed**
Some nodes generate random values, and to generate random values we generally need a seed value.
**settings**
There are some values that are kind-of global inputs. That means they are inputs to the node, but each node receives the same input. For example for the plant nodes, they are the resolution of each leave.
After some thinking, my idea is to model these inputs as *hidden* inputs to the node. So they are not gonna be visible in the **NODE_INTERFACE**.
But they are gonna be passed to the nodes as normal inputs. This could be super useful to allow easy memoization of the nodes, because if none of the inputs changed, we can return the previously computed output.
## Linking/Importing of Nodes
**WebAssembly Components**
\- only works in wasmtime / jco at the moment
For this the runtime_executor needs to be wasm aswell.
**No linking**
\+ Very easy to implement, works everywhere
2024-03-20 17:22:37 +01:00
\- Probably not as fast as components
# Handle multiple outputs
The general idea is to model the nodes as a visual representation of pure functions, that means the function only operates on its own inputs, doesnt have any side effects and always produces the same output for the same input.
For very simple graph, such as the following this works very well.
The `math/add` node could be modelled very simple as the following pure function:
```rust
fn math_add(a: i32, b:i32) -> i32 {
return a + b
}
```
![](PURE_GRAPH.png)
The problem occurs if we try do adher to this principle for more complex use cases.
## Problem Example 1
![](RANDOM_PROBLEM.png)
Users would probably expect the random node to output for different results. If we would model the `random` node as a pure function, it does not know how many results it should generate.
## Problem Example 2
![](NOISE_PROBLEM.png)
On the left you see a node setup for the procedural generation of geometry. On the right one of the probably outcomes a user would expect.
If we model the noise function as a pure function we would first have to figure out how to pass the result of the noise function as a pure value to the `cylinder` node. The `noise` node also has no idea how many outputs are connected to it.
## Solution 1 (Power to the Nodes)
We could solve this by letting each node execute its parent node graph however it wants. This would allow the `cylinder` node to execute the `noise` however often it needs to create the cylinders.
```typescript
// how this could look like in the node
{
id: "cylinder",
inputs: {
"amount": { type: "integer", value: 1 },
"height": { type: "float" },
"radius": { type: "float" },
},
execute: ({ amount, height, width }, executeNodeGraph) => {
const stemAmount = executeNodeGraph(amount);
const ringAmount = 10;
for(let i = 0; i < stemAmount; i++){
for(let x = 0; x < ringAmount; x++){
const radius = executeNodeGraph(width);
// create a single ring
}
}
}
}
```
**Pros**
- nodes become very flexible on how the work in their inputs
**Cons**
- Every Nodes needs access to the [[Projects/bachelor/RUNTIME_EXECUTOR]]
- It could become very unpredictable how each node behaves, because the each have a own way to handle inputs
## Solution 2 (Parametrized nodes, embed execution)
We could change the output of the noise function to output a definition of noise like the following:
```json
{
"type": "noise",
"size": 0.7,
"strength": 1.5
}
```
Or the random node could output something like this:
```json
{
"type": "random",
"min": 0,
"max": 42
}
```
Then we could embed a function that nows how to turn these parametrized nodes into numbers inside the `cylinder` node. One problem with this is that this function needs to be recursive, because the inputs of the parametrized nodes could also be other parametrized nodes:
```json
{
"type": "noise",
"size": 0.7,
"strength": {
"type": "random",
"min": 0,
"max": 42
}
}
}
```
**Pros**
- The overhead inside the `cylinder` node is smaller than the other solutions
- BIIIG Pro; the runtime knows nothing about that
**Cons**
- We basically have to embed the execution function of number nodes inside other nodes
- recursive execution of the nodes definitely needs memoization
## Solution 3 (Partially invert the flow)
Normally the flow of information in this graph is only from the parents to the children nodes. One idea could be for the children nodes to send to their parents what they need, and then have the parents return that result to their children.
## Solution 4 (Add execution information to parameters)
We could also add metadata to the node definition to tell the [[Projects/bachelor/RUNTIME_EXECUTOR]]. This could either be how it should execute the parent nodes of a node, or directly in the node how it should be executed.
For example in the first [[#Problem Example 1|Example 1]] it could look like this
```typescript
const randomNode = {
id: "random",
inputs: {
"value": { type: "float", value: 0.1, internal: true },
"seed": { type:"runtime_seed" }
},
outputs: ["float"],
execution: {
// this is the important part
distinctOutput: true
},
execute: ({ value, seed }) => {
return value
}
}
```
This could tell the [[Projects/bachelor/RUNTIME_EXECUTOR]] to execute the `randomNode` for every of its child nodes individually.
---
# Parameters Dimensionality
We could define how many dimensions a parameter can handle and which range it could handle. For example the `radius` parameter could be a 2D parameter, and we could map the x coordinate to the angle relative to the center of the stem and y to the height along the stem.
The `noise` node would output a 2D parameter. We sample it with 2 coordinates. But we could also only sample it with one coordinate.
If we would have a theoretical `curve` function, it would produce a `1D` parameter which is sampled with a x coordinate from 0 to 1.
The `random` node outputs a `1D` parameter, we sample it with the `seed`.
Then we would need a way to sample this parameter where we need the value.
```typescript
const cylinderNode = {
id: "cylinder",
outputs: ["geometry"],
inputs: {
height: { type: "float" },
radius: { type: "float" },
amount: { type: "integer" },
seed: { type: "runtime_seed" },
xResolution: { type:"integer", setting:"resolution.x" },
yResolution: { type:"integer", setting:"resolution.y" },
},
execute({ height, radius, amount, xResolution, yResolution, seed }){
const coordinates: number[][] = [];
const angleIncrement = (2 * Math.PI) / xResolution;
const amount = sampleParameter1D(amount);
for (let j = 0; j < amount; j++){
const cylinder: number[] = []
// here we sample a 1D Parameter
const heightIncrement = sampleParameter1D(height, j) / yResolution;
for (let i = 0; i < yResolution; i++) {
const y = i * heightIncrement;
for (let j = 0; j < xResolution; j++) {
const angle = j * angleIncrement;
const radius = sampleParameter2D(radius, y, angle);
const x = radius * Math.cos(angle);
const z = radius * Math.sin(angle);
coordinates.push( , y, z);
}
}
coordinates.push(cylinder);
}
return coordinates;
}
}
```