WIP: Add a math expression geometry node #115112

Draft
Andrew-Downing wants to merge 19 commits from Andrew-Downing/blender:node_geo_math_expression into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
First-time contributor

This adds a geometry node that parses and evaluates a user defined math expression. The number of input sockets, output socket and types are determined dynamically based on user input.

Currently the types and number of input sockets are based on a separate string property containing a list of variables. Due to limitations of the node system, this needs to be parsed in a way that can't fail since I see no way to display error messages in the declare callback. Variables names are simply parsed as words separated by non-word characters. Duplicates and words that don't start with either 'v' or 'f' are ignored. Variables that start with 'v' create a Vector input socket, and ones that start with 'f' create a Float input socket. This also means that other types can be added in the future without breaking files made with older versions lacking those types.

I think determining the inputs from something separate from the expression itself is a good idea. Imagine that you're using this node and its not doing what you expect, so you tweak the expression and perhaps remove a variable. It would be pretty annoying if doing that removed the input socket and disconnected connections if you needed to add it right back. Not all inputs need to be used in the expression, but it would be a good idea to show a warning for unused inputs.

Function names and variable names exist in a separate namespace so functions can be added in the future without breaking files that are already using variables with the same name.

Currently supported expressions:

  • expr means any expression
  • float and vec represent a value of type Float and Vector in the node editor.
  • expr -> float and expr -> vec mean the expression to the left evaluates to a value of the type on the right.

Variables:

  • Must start with 'f' or 'v' and contain only underscores or alphanumeric characters.

variable-name-starting-with-f -> float
variable-name-starting-with-v -> vec

Constants:
pi -> float
tau -> float
e -> float

Numbers:
1 -> float
1.1 -> float
1. -> float
.1 -> float

Operators:
float + float -> float
vec + vec -> vec

float - float -> float
vec - vec -> vec
-float -> float
-vec -> vec

float * float -> float
float * vec -> vec
vec * float -> vec

float / float -> float
vec / float -> vec

Functions:

  • All functions in "NOD_math_functions.hh" have been added.
  • For now, the names are the part of the enum value names after "NODE_MATH_" or "NODE_VECTOR_MATH_" converted to lowercase. e.g. NODE_VECTOR_MATH_CROSS_PRODUCT is named cross_product. Some of these should probably be shortened.
  • Some of these should probably be removed. There isn't really a point to have an add and subtract function when those operations are also covered by the + and - operators.
  • Some of the names should probably be changed. e.g. there is no canonical multiply or division operation between vectors (this is also why I did not add a vec * vec or vec / vec operator), so the multiply and divide function might be better named hadamard_mul and hadamard_div or something.

Additional custom functions:
lerp(float, float, float) -> float
lerp(vec, vec, float) -> vec

vec(float, float, float) -> vec

x(vec) -> float
y(vec) -> float
z(vec) -> float

Uniform function call syntax:
expr.function_name -> function_name(expr)
expr.function_name() -> function_name(expr)
expr1.function_name(expr2) -> function_name(expr1, expr2)

This adds a geometry node that parses and evaluates a user defined math expression. The number of input sockets, output socket and types are determined dynamically based on user input. Currently the types and number of input sockets are based on a separate string property containing a list of variables. Due to limitations of the node system, this needs to be parsed in a way that can't fail since I see no way to display error messages in the declare callback. Variables names are simply parsed as words separated by non-word characters. Duplicates and words that don't start with either 'v' or 'f' are ignored. Variables that start with 'v' create a Vector input socket, and ones that start with 'f' create a Float input socket. This also means that other types can be added in the future without breaking files made with older versions lacking those types. I think determining the inputs from something separate from the expression itself is a good idea. Imagine that you're using this node and its not doing what you expect, so you tweak the expression and perhaps remove a variable. It would be pretty annoying if doing that removed the input socket and disconnected connections if you needed to add it right back. Not all inputs need to be used in the expression, but it would be a good idea to show a warning for unused inputs. Function names and variable names exist in a separate namespace so functions can be added in the future without breaking files that are already using variables with the same name. **Currently supported expressions:** - `expr` means any expression - `float` and `vec` represent a value of type Float and Vector in the node editor. - `expr` -> `float` and `expr` -> `vec` mean the expression to the left evaluates to a value of the type on the right. **Variables:** - Must start with 'f' or 'v' and contain only underscores or alphanumeric characters. _`variable-name-starting-with-f`_ -> `float` _`variable-name-starting-with-v`_ -> `vec` **Constants:** `pi` -> `float` `tau` -> `float` `e` -> `float` **Numbers:** `1` -> `float` `1.1` -> `float` `1.` -> `float` `.1` -> `float` **Operators:** `float + float` -> `float` `vec + vec` -> `vec` `float - float` -> `float` `vec - vec` -> `vec` `-float` -> `float` `-vec` -> `vec` `float * float` -> `float` `float * vec` -> `vec` `vec * float` -> `vec` `float / float` -> `float` `vec / float` -> `vec` **Functions:** - All functions in "NOD_math_functions.hh" have been added. - For now, the names are the part of the enum value names after "NODE_MATH_" or "NODE_VECTOR_MATH_" converted to lowercase. e.g. `NODE_VECTOR_MATH_CROSS_PRODUCT` is named `cross_product`. Some of these should probably be shortened. - Some of these should probably be removed. There isn't really a point to have an `add` and `subtract` function when those operations are also covered by the `+` and `-` operators. - Some of the names should probably be changed. e.g. there is no canonical multiply or division operation between vectors (this is also why I did not add a `vec * vec` or `vec / vec` operator), so the `multiply` and `divide` function might be better named `hadamard_mul` and `hadamard_div` or something. **Additional custom functions:** `lerp(float, float, float)` -> `float` `lerp(vec, vec, float)` -> `vec` `vec(float, float, float)` -> `vec` `x(vec)` -> `float` `y(vec)` -> `float` `z(vec)` -> `float` **Uniform function call syntax:** `expr.function_name` -> `function_name(expr)` `expr.function_name()` -> `function_name(expr)` `expr1.function_name(expr2)` -> `function_name(expr1, expr2)`
Andrew-Downing added 13 commits 2023-11-18 22:01:11 +01:00
Andrew-Downing requested review from Jacques Lucke 2023-11-19 18:57:28 +01:00
Andrew-Downing added 1 commit 2023-11-19 18:59:15 +01:00
Andrew-Downing added 1 commit 2023-11-19 21:28:37 +01:00
Author
First-time contributor

I've added uniform function call syntax so that foo.bar and foo.bar() are the same as bar(foo) and foo.bar(1) would be the same as bar(foo, 1). This makes things like accessing the components of a vector a little cleaner looking, v1.x instead of x(v1).

I've added uniform function call syntax so that foo.bar and foo.bar() are the same as bar(foo) and foo.bar(1) would be the same as bar(foo, 1). This makes things like accessing the components of a vector a little cleaner looking, v1.x instead of x(v1).
Member

I think this patch goes generally in the right direction. I didn't do an in-depth review yet, but do have some initial notes:

  • Use clang format for the code formatting.
  • Use Blenders C++ containers such as BLI_map.hh and BLI_set.hh.
  • I think eventually the expression should be a string input and the set of variables should be stored in the node similarly to e.g. dynamic repeat zone sockets.
  • The patch description should have a more comprehensive list of what is allowed in an expression that is updated as things change.
  • While uniform function call syntax is nice, I think we should be more careful initially with adding to many ways to do the same thing. That makes it harder to change things later on. Differentiating between method calls and properties access (e.g. v.x) seems reasonable.
  • That's more for me: this patch makes it more important that we have some kind of global registry for some commonly used multi-functions to avoid compiling them multiple times.
I think this patch goes generally in the right direction. I didn't do an in-depth review yet, but do have some initial notes: * Use [clang format](https://wiki.blender.org/wiki/Tools/ClangFormat) for the code formatting. * Use Blenders C++ containers such as `BLI_map.hh` and `BLI_set.hh`. * I think eventually the expression should be a string input and the set of variables should be stored in the node similarly to e.g. dynamic repeat zone sockets. * The patch description should have a more comprehensive list of what is allowed in an expression that is updated as things change. * While uniform function call syntax is nice, I think we should be more careful initially with adding to many ways to do the same thing. That makes it harder to change things later on. Differentiating between method calls and properties access (e.g. `v.x`) seems reasonable. * That's more for me: this patch makes it more important that we have some kind of global registry for some commonly used multi-functions to avoid compiling them multiple times.
Jacques Lucke requested review from Hans Goudey 2023-11-20 10:05:56 +01:00
Andrew-Downing added 1 commit 2023-11-20 16:54:29 +01:00
Andrew-Downing added 1 commit 2023-11-20 19:42:11 +01:00
Andrew-Downing added 1 commit 2023-11-22 20:34:46 +01:00
Andrew-Downing added 1 commit 2023-11-24 20:09:23 +01:00

We have an expression evaluation used for drivers, based on a simple subset of python. I think we should use the same syntax for both drivers and this expression node.
https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenlib/intern/expr_pylike_eval.c

Perhaps it is possible to share the parsing code, while using a different evaluation mechanism.

We have an expression evaluation used for drivers, based on a simple subset of python. I think we should use the same syntax for both drivers and this expression node. https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenlib/intern/expr_pylike_eval.c Perhaps it is possible to share the parsing code, while using a different evaluation mechanism.
Member

I think we should use the same syntax for both drivers and this expression node.

Yeah, we came to the same conclusion on Tuesday: https://devtalk.blender.org/t/2023-11-28-nodes-physics-module-meeting/32306

I haven't used drivers myself much yet, but it doesn't seem to provide clear guidelines for us for how vector math should work, does it? Like, how do you do you compute a dot/cross product, do element wise multiplication, get the vector length and access individual components?

> I think we should use the same syntax for both drivers and this expression node. Yeah, we came to the same conclusion on Tuesday: https://devtalk.blender.org/t/2023-11-28-nodes-physics-module-meeting/32306 I haven't used drivers myself much yet, but it doesn't seem to provide clear guidelines for us for how vector math should work, does it? Like, how do you do you compute a dot/cross product, do element wise multiplication, get the vector length and access individual components?

Drivers are evaluated with the simple expression parsers when possible, and if not they are evaluated as Python code. The expression parser does not support vectors, and drivers generally work on individual floats anyway.

But in general you can use mathutils.Vector inside them.
https://docs.blender.org/api/current/mathutils.html#mathutils.Vector

So that means you have syntax like a.x, a.length, a.dot(b).

Drivers are evaluated with the simple expression parsers when possible, and if not they are evaluated as Python code. The expression parser does not support vectors, and drivers generally work on individual floats anyway. But in general you can use `mathutils.Vector` inside them. https://docs.blender.org/api/current/mathutils.html#mathutils.Vector So that means you have syntax like `a.x`, `a.length`, `a.dot(b)`.
Author
First-time contributor

I looked at expr_pylike_eval before making this, but it didn't really seem appropriate for this use. The lexer and parser here is probably the least complex part of all of this by far. Regardless of what syntax is used, hardly anything needs to change in the lexer or parser. It also already has good error reporting, I didn't see any in the pylike parser. It's also much less code.

For the syntax I envisioned whatever the standard way to write math on a single line with ascii characters is, if such a thing exists. For what is currently implemented, I think this is already basically that. Only the +, -, /, and * operators and functions are implemented, so the only real knob left to turn is the names of the functions. I purposely left out an exponentiation operator for now because there is some question as to what is the "standard" way to do that. In my opinion it would either be using x^y or pow(x, y), x**y would be very opinionated.

Besides exponentiation, I think the syntax is already the same as the pylike parser if I just change the names of the functions. If a is a vector then a.x, a.length, and a.dot(b) already work.

I looked at expr_pylike_eval before making this, but it didn't really seem appropriate for this use. The lexer and parser here is probably the least complex part of all of this by far. Regardless of what syntax is used, hardly anything needs to change in the lexer or parser. It also already has good error reporting, I didn't see any in the pylike parser. It's also much less code. For the syntax I envisioned whatever the standard way to write math on a single line with ascii characters is, if such a thing exists. For what is currently implemented, I think this is already basically that. Only the +, -, /, and * operators and functions are implemented, so the only real knob left to turn is the names of the functions. I purposely left out an exponentiation operator for now because there is some question as to what is the "standard" way to do that. In my opinion it would either be using `x^y` or `pow(x, y)`, `x**y` would be very opinionated. Besides exponentiation, I think the syntax is already the same as the pylike parser if I just change the names of the functions. If `a` is a vector then `a.x`, `a.length`, and `a.dot(b)` already work.

The reason to share the code is not so much to reduce the amount of code, but to ensure the implementations are compatible and stay that way. If your lexer and parser implementation is better it can be used for the drivers too.

And then we can have a single documentation page for expression syntax, and users can copy them between geometry nodes, drivers and Python code.

I imagine if you put the implementations side by side there are various differences.

The reason to share the code is not so much to reduce the amount of code, but to ensure the implementations are compatible and stay that way. If your lexer and parser implementation is better it can be used for the drivers too. And then we can have a single documentation page for expression syntax, and users can copy them between geometry nodes, drivers and Python code. I imagine if you put the implementations side by side there are various differences.
Member

Are there plans for including matrix math in these?

Are there plans for including matrix math in these?
Author
First-time contributor

Are there plans for including matrix math in these?

I've thought about that and rotations, but haven't really looked into it yet. I don't think there is a matrix socket type yet though is there?

> Are there plans for including matrix math in these? I've thought about that and rotations, but haven't really looked into it yet. I don't think there is a matrix socket type yet though is there?
Author
First-time contributor

The reason to share the code is not so much to reduce the amount of code, but to ensure the implementations are compatible and stay that way. If your lexer and parser implementation is better it can be used for the drivers too.

And then we can have a single documentation page for expression syntax, and users can copy them between geometry nodes, drivers and Python code.

I imagine if you put the implementations side by side there are various differences.

You mean a subset of the expression syntax would be able to be copied between them right? From the driver docs it looks like using e.g. self is supported in some way. That wouldn't be supported in this node because it's not python, and doesn't fall back to executing as python code. It also looks like there is a frame global variable which wouldn't exist in this node. This node could implement the "Simple Expressions" syntax of drivers, but there are additional functions that would be implemented too that aren't part of that syntax, so you wouldn't really be able to copy all simple expressions between them. The only thing that would really be able to be copied between them are expression that consist only of functions that are common between the two, operators, pi, True, False, and number literals.

To use the same parser for this node and drivers, the parser needs to generate a different "thing" depending the context of what its parsing. For drivers it looks like it needs to generate a of list of instructions that run in a simple VM. For this node it generates a tree of Expression objects.

I'm not sure if it would be preferable to have the parser generate an Expression tree in both contexts, and just add a function to the Expression classes to evaluate an expression to a double in addition to the compile function that compiles an Expression tree to fields and multi-functions. The driver code could then use this instead of the VM type evaluator.

> The reason to share the code is not so much to reduce the amount of code, but to ensure the implementations are compatible and stay that way. If your lexer and parser implementation is better it can be used for the drivers too. > > And then we can have a single documentation page for expression syntax, and users can copy them between geometry nodes, drivers and Python code. > > I imagine if you put the implementations side by side there are various differences. You mean a subset of the expression syntax would be able to be copied between them right? From the driver docs it looks like using e.g. `self` is supported in some way. That wouldn't be supported in this node because it's not python, and doesn't fall back to executing as python code. It also looks like there is a `frame` global variable which wouldn't exist in this node. This node could implement the "Simple Expressions" syntax of drivers, but there are additional functions that would be implemented too that aren't part of that syntax, so you wouldn't really be able to copy all simple expressions between them. The only thing that would really be able to be copied between them are expression that consist only of functions that are common between the two, operators, pi, True, False, and number literals. To use the same parser for this node and drivers, the parser needs to generate a different "thing" depending the context of what its parsing. For drivers it looks like it needs to generate a of list of instructions that run in a simple VM. For this node it generates a tree of `Expression` objects. I'm not sure if it would be preferable to have the parser generate an `Expression` tree in both contexts, and just add a function to the `Expression` classes to evaluate an expression to a `double` in addition to the `compile` function that compiles an `Expression` tree to fields and multi-functions. The driver code could then use this instead of the VM type evaluator.

frame could exist in this node too, but indeed there would be no self.

I don't image users will often copy expressions verbatim, usually you'd modify it a bit, but the closer it can be the easier. What we want to avoid is some syntax or function name being arbitrarily different, and that causing confusion.

For drivers, maybe it would be best to generate more or less the same data structures so BLI_expr_pylike_eval works the same way. That is a flat array of opcodes. It may have better performance than the expression tree, in terms of memory locality, virtual function calls, etc.

`frame` could exist in this node too, but indeed there would be no `self`. I don't image users will often copy expressions verbatim, usually you'd modify it a bit, but the closer it can be the easier. What we want to avoid is some syntax or function name being arbitrarily different, and that causing confusion. For drivers, maybe it would be best to generate more or less the same data structures so `BLI_expr_pylike_eval` works the same way. That is a flat array of opcodes. It may have better performance than the expression tree, in terms of memory locality, virtual function calls, etc.
First-time contributor

I see in the image that the outputs is decided by a dropdown. (maybe you changed it already. anyway,)
I think I would be a good idea to have multiple outputs. perhaps a seperate text field thar you type the outputs variables you want, and they adopt their type automatically.

I see in the image that the outputs is decided by a dropdown. (maybe you changed it already. anyway,) I think I would be a good idea to have multiple outputs. perhaps a seperate text field thar you type the outputs variables you want, and they adopt their type automatically.
This pull request has changes conflicting with the target branch.
  • source/blender/blenkernel/BKE_node.h
  • source/blender/makesdna/DNA_node_types.h
  • source/blender/makesrna/intern/rna_nodetree.cc
  • source/blender/nodes/NOD_static_types.h
  • source/blender/nodes/geometry/CMakeLists.txt

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u node_geo_math_expression:Andrew-Downing-node_geo_math_expression
git checkout Andrew-Downing-node_geo_math_expression
Sign in to join this conversation.
No reviewers
No Label
Interest
Alembic
Interest
Animation & Rigging
Interest
Asset Browser
Interest
Asset Browser Project Overview
Interest
Audio
Interest
Automated Testing
Interest
Blender Asset Bundle
Interest
BlendFile
Interest
Collada
Interest
Compatibility
Interest
Compositing
Interest
Core
Interest
Cycles
Interest
Dependency Graph
Interest
Development Management
Interest
EEVEE
Interest
EEVEE & Viewport
Interest
Freestyle
Interest
Geometry Nodes
Interest
Grease Pencil
Interest
ID Management
Interest
Images & Movies
Interest
Import Export
Interest
Line Art
Interest
Masking
Interest
Metal
Interest
Modeling
Interest
Modifiers
Interest
Motion Tracking
Interest
Nodes & Physics
Interest
OpenGL
Interest
Overlay
Interest
Overrides
Interest
Performance
Interest
Physics
Interest
Pipeline, Assets & IO
Interest
Platforms, Builds & Tests
Interest
Python API
Interest
Render & Cycles
Interest
Render Pipeline
Interest
Sculpt, Paint & Texture
Interest
Text Editor
Interest
Translations
Interest
Triaging
Interest
Undo
Interest
USD
Interest
User Interface
Interest
UV Editing
Interest
VFX & Video
Interest
Video Sequencer
Interest
Virtual Reality
Interest
Vulkan
Interest
Wayland
Interest
Workbench
Interest: X11
Legacy
Blender 2.8 Project
Legacy
Milestone 1: Basic, Local Asset Browser
Legacy
OpenGL Error
Meta
Good First Issue
Meta
Papercut
Meta
Retrospective
Meta
Security
Module
Animation & Rigging
Module
Core
Module
Development Management
Module
EEVEE & Viewport
Module
Grease Pencil
Module
Modeling
Module
Nodes & Physics
Module
Pipeline, Assets & IO
Module
Platforms, Builds & Tests
Module
Python API
Module
Render & Cycles
Module
Sculpt, Paint & Texture
Module
Triaging
Module
User Interface
Module
VFX & Video
Platform
FreeBSD
Platform
Linux
Platform
macOS
Platform
Windows
Priority
High
Priority
Low
Priority
Normal
Priority
Unbreak Now!
Status
Archived
Status
Confirmed
Status
Duplicate
Status
Needs Info from Developers
Status
Needs Information from User
Status
Needs Triage
Status
Resolved
Type
Bug
Type
Design
Type
Known Issue
Type
Patch
Type
Report
Type
To Do
No Milestone
No project
No Assignees
5 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender#115112
No description provided.