JacquesLucke / animation_nodes

Node based visual scripting system designed for motion graphics in Blender.
Other
2.28k stars 342 forks source link

A question about variable nodes #1026

Closed mathlusiverse closed 5 years ago

mathlusiverse commented 5 years ago

My animation depends on the value of a certain vector V, which changes very slowly. The calculation of V is time-consuming. I don't want to calculate V at every frame. I want to update (calculate) V every 15th frame, save the value to a variable and retrieve it in all intermediate frames.

It does not seem possible to declare a variable node to store and retrieve variables. The variables (integer, float, vector etc) should be persistent and accessible by all nodes (at least in the same tree) across different frames. If not possible, I like to suggest implementing variable nodes. I believe it will be very powerful for many other situations.

OmarEmaraDev commented 5 years ago

Wouldn't a per frame cache be ok in this case?

Clockmender commented 5 years ago

I have already done this in several of my own nodes. I am on holiday just now (in Vietnam without my Mac), but will let you know how I did it when I get home mid February . Basically you can set a “property” to be a variable and update it on the fly and read it whenever you want, these are displayed with a “draw” option in the node definition. You can also do this with custom properties, but “variable” nodes are better IMHO.

Cheers, Clock

mathlusiverse commented 5 years ago

Thanks for the reply.

Can @OmarSquircleArt explain per frame cache? Is the cache per frame or per whole animation (meaning persist between different frames)?

Looking forward to see how @Clockmender achieve the it.

Without seeing either of the solutions yet, I tend to think a dedicated "variable" node will provide a clean and transparent (and hence better) approach to situations such as my use case. :)

OmarEmaraDev commented 5 years ago

Have a look at the documentation for subprogram caching. Basically, you create a group that computes the vector V. And enable Per Frame Caching Mechanism to cache the result of the group at different frames. Alternatively, you can use a Per Input Caching Mechanism to get the exact behaviour you are looking for, this works as follows:

Node Tree

This will compute the vector once every 15 frames. Of course, the input frame has to be multiplied by 15 in this case during computation.

mathlusiverse commented 5 years ago

@OmarSquircleArt Thanks for the picture and the link. I read the documentation, it seems 'Once Per Input' is the appropriate cache mode here.

Your idea of feeding floor(frame/15) as the input will indeed skip the calculation for most of the frames and force the computation every 15 frames. The problems is it will accumulate old and outdated cache and waste memory.

There is absolutely no need to cache old values of the vector V (referring to my example). The value V is meant to be overwritten at every new input. I see the button 'Clear cache' in the picture. Is it possible to 'pro grammatically press' the button inside [Do something expensive here] before each calculation?

If not, then this may not be what I want. BTW, thanks for pointing out the cache feature. I didn't know it before, I am sure I will need it for other purpose later.

OmarEmaraDev commented 5 years ago

There is no way to achieve this in Animation Nodes currently, aside from using a python script or using some strange hacks. I might submit a patch that gives some functionality like this, but currently there is no way to do this that I know of. Sorry.

mathlusiverse commented 5 years ago

Thanks for the info.

It seems that my use case is common and important enough to warrant a special node. One immediate approach is to wrap the Group Input node with auto clear cache at each new input. But this is not a desirable solution because it is essentially disabling a sophisticated feature (caching of multiple input version) to achieve overwriting (caching only the latest version disregarding the input combination).

It also seems it would (it will take time to debug, optimize and polish) be easy to strip the extra features of Group Input node and turn it into a simple "variable node": (1) We don't need cache for multiple input combination. (2) We don't need multiple input parameters.

I am not familiar with the implementation of Animation Nodes. Sorry if the following comment is way off.

The implementation of "variable node" should be easier and more efficient than Group Input node. Because it should just have the following 3 essential ingredients: (1) Name of the node (serve as the name of the variable) (2) A simple trigger for writing value (instead of comparing new input with cached input combinations) . (3) An always-available output socket for reading the value.

Of course, a totally different approach to the whole concept of "variable node" may be worth consideration. But in the main time, the above 'dirty fix' should work. :)

OmarEmaraDev commented 5 years ago

But how do you want to implement this trigger? We will still need some method to disable the execution of the node tree responsible for creating the variable data. Or else the node will be of no use.

mathlusiverse commented 5 years ago

A pure "variable node" should not link execution code for calculating its own value. It should be just a container that accepts calculated value and make it available to others.

The Group Input/Output nodes are actually more than a cache, together it is like an execution module with a buffer for writing/reading its internal state.

We can achieve "variable node" by having one boolean input only. For example, we can feed (frame % 15==0) as the input. It is a trigger. The group input will execute the nodes between Group Input/Output only if the trigger is true.

Now how do we get the parameters needed for the calculation of variable? Because the Group Input/Output is responsible for launching and providing all necessary data for the calculation. Hence "extra" inputs such as frame (in my example) will be part of the Group Input setup!

This is ugly approach! In this case, the data "frame" is actually globally available, so we don't really need to include it in the Group Input setup. But what if the variable requires a lot of different data? It will be ugly to provide everything in the Group Input setup. In such case, may be we should have a stand-alone function to calculate the value of a variable, and have the Group Input/Output pair to act on a single boolean trigger and then call the function to update its own internal cache.

I believe, we need pure container nodes for holding variable values. They should not be associated with the calculation in any manner. This is a conceptually better approach, because a variable may be calculated by different methods in different places at different time under different circumferences. To have the Group Input/Output to embed all these different ways to update the variable is just not clean approach.

OmarEmaraDev commented 5 years ago

I see what you mean. Subprogram weren't designed for this purpose anyway. While a specialized subprogram needs more study, what we can do currently is add a boolean input to the Invoke node to trigger its execution. I already have such system in my custom build. I will discuss it with Jacques and see if we can merge this upstream.

mathlusiverse commented 5 years ago

Great! :)
I think that will do the job.

I do hope you guys will consider implementing a true variable node, that are stand-alone, and can be used anywhere in a tree for writing and reading.

I will try look into the implementation and happy to help down the road.

OmarEmaraDev commented 5 years ago

A good idea would be to open a new issue with clear description of your proposal. That way Jacques can read it more clearly and give input.

mathlusiverse commented 5 years ago

OK, basically a variable node should be a simple wrapper of a Python variable. Its usage and behavior should be intuitive and non-surprising to new comers of the Animation Nodes. It should be robust and conceptually-simple enough that it does not have unexpected side effect when it is used anywhere by reasonable new users.

Thanks for the suggestion. I'll have to think about how to write the proposal. It will probably take me a while to do that.

mathlusiverse commented 5 years ago

@Clockmender, It sounds like you add a custom property to an object and use it as a variable. I am not very familiar with Animation Nodes, I am curious how you actually do that. This may turn out the 'cheapest' way to do variable, please post your approach also when you come back from your trip!

Enjoy your trip! :)

Clockmender commented 5 years ago

OK a quick starter, no images as it’s from my phone:

Create an object and add a Custom Property of some description and call it “Value_1”.

Add an Object node and select the new object.

Add an Expression Node and plug the object node into the input socket.

Type this into the Expression Node:

x[‘Value_1’]

Plug a Viewer into the output socket, set some value for the custom property in the normal way and execute your node tree. You will see the custom property value output to the viewer node.

That’s the easiest way to read custom properties into a node tree. If you type this into the Expression Node, assuming a float property:

x[‘Value_1’] = 12.56

And execute the node tree it will set the custom property to the value input.

You are quite correct the use modulo maths to trigger responses every so many frames, you could experiment with an expression node here also. Expressions like:

0 if frm % 15 == 0 else frm

Are perfectly valid if you feed the frame input into the Expression node and call the input “frm”.

Cheers, Clock

Edit

I am quite happy to make a node for you when I get home, but this should get you thinking in the right direction. More later....

This, used as an expression:

x[‘Value_1’] = x[‘Value_1’] if frm % 15 != 0 else x[‘Value_1’] + 1

Should increase the value of the custom property by 1 every 15 frames, if my memory serves me well, I cannot test this....

mathlusiverse commented 5 years ago

@Clockmender I tried to follow your suggestions, and tried some variations but did not get the expected result. The viewers all show "None".

It is surprising that the custom property must be float type, or am I looking at the wrong tabs?

Thanks for the hints! I will try writing a node to solve this problem.

image

OmarEmaraDev commented 5 years ago

@mathlusiverse Animation Nodes is not functioning in this screenshot. You are probably using an old AN version where things didn't work due to API change. Try a newer version.

mathlusiverse commented 5 years ago

@OmarSquircleArt I downloaded the following https://github.com/JacquesLucke/animation_nodes/archive/v2.1_blender2.8_testbuild1.zip from Animation Nodes 2.1 for Blender 2.8 Test Builds. It is the latest one that I can find. BTW is the test build has a github?

I am using the nightly built Blender 2.8 and Python 3.7. @JacquesLucke was VERY helpful in troubleshooting my setup. The Animation Nodes was freshly built and copy to Blender addon folder a few hours ago.

I assume I am working with the latest version of Animation Nodes. It said version 2.1.3 May be I am missing something.

OmarEmaraDev commented 5 years ago

@mathlusiverse This is not the latest. Just clone the blender2.8 branch and build it directly. The files on the release page are out-dated.

mathlusiverse commented 5 years ago

@OmarSquircleArt Thanks! I am looking for the Github for Blender2.8 built! :)

I am new at GitHub, I just selected the blender2.8 branch, cloned the blender2.8 branch using Github desktop. I rebuild the whole thing. Now Blender complainted the AN is version 2.0.4.

This may be a silly question :( I selected branch 2.8 before I clone it with Github desktop, but when I build everything, it seems to use the old branch. How do I actually get branch28 to my local drive?

In the mean time, I am trying different ways to get branch28 onto my drive, I am not sure what I am doing LOL. I get the following "This may never happen" bug. It may be just my system is confused with different version.

image

OmarEmaraDev commented 5 years ago

All branches are stored in this repository, this is how git works. When you clone a repository, it is automatically checkout at the master branch. So you have to checkout to the blender2.8 branch. To do that, you should navigate to the repository and execute git checkout blender2.8, I am not sure how to do that on Github Desktop, but a quick google search tells me:

Simply click on the dropdown menu on the upper left hand corner of the client, and pick the branch you want to switch to.

After checking out, you should be able to to build it with no problems.

mathlusiverse commented 5 years ago

It is working now!!! Thanks @OmarSquircleArt Now I am building from the blender2.8 in the Github. That's really an adventure.

The idea of using an expression to get the value of a custom property from an Object input node is not working. Not sure what the two cryptic error message mean (Wrong Output Type & Expected Object). LOL I think it is probably just the wrong usage of the nodes, not anything wrong with the nodes themselves.

But the other method work. However, the custom property is float only. I am going to explore how to write/read a simple node storing a Vector value.

image

OmarEmaraDev commented 5 years ago

The expression node have a generic output socket by default, you changed that to an Object socket. So Animation Nodes tried to convert the float to an object and fails, hence the error. To change the output to the desired type, click on the gear button.

I think you might find my answer here useful.

mathlusiverse commented 5 years ago

Yes. It work. I clicked the gear before, but did not realize it is for selecting type of the value. It makes perfect sense now.

OmarEmaraDev commented 5 years ago

@mathlusiverse I made a pull request for a conditional invoking feature #1027. You can compile the conditional_invoking branch to use it. Can you test it?

Clockmender commented 5 years ago

@omar... thanks for helping with this!

@math... you might like to think about storing three floats on your node and using the execute function to combine them into a vector for the output socket. I am not sure there is a vector property you can use in your node. I would use FloatProperties for your variables, float inputs to set them and a BoolProperty to set the variables to the inputs, so only change vector variables if BoolProperty is True.

I will start work with you on this when I get home.

Cheers, Clock.

Clockmender commented 5 years ago

I got one of my friends to do this for you and mail me this picture:

screenshot 2019-02-09 at 10 02 42

It was done in blender 2.79 with AN 2.0, but gives you a better idea of what I was rambling on about in a previous post. It resets the custom property at frame 1 back to 0 and increments it by 1 every 15 frames as you run the animation. Building this into an "easy to use" node will not be difficult.

Cheers, Clock.

mathlusiverse commented 5 years ago

@OmarSquircleArt I tested the conditional invoke with a very simple animation. It works. Thanks!

Please see the attached blender file. The animation consists of 2 planets (yellow cube and blue sphere) moving around its own circular orbit. The cube moves every 20 frames, the sphere also moves every 20 frames, but delayed by 10 frames. Hence they kind of 'jump' (not smooth move because of the 20 frames pause) alternatively.

The position of a planet is (Rcos A, Rsin A, 0) where R is orbit radius, A is the angle determined by the speed.

ao-121a.zip

image

BTW, Why are the color of the frames shaded?All those vibrant colors are useless. I like bright yellow frames :)

mathlusiverse commented 5 years ago

@Clockmender Thanks for the replies. They look interesting.

I am going out of town with my kids for this weekend. I will look at your ideas as soon as I have time. Thanks again.

OmarEmaraDev commented 5 years ago

@mathlusiverse You can change their color in the N Panel.

mathlusiverse commented 5 years ago

@Clockmender I am new to Animation Nodes. The nodes look unfamiliar in the diagram attached in https://github.com/JacquesLucke/animation_nodes/issues/1026#issuecomment-462008524 I'll try figuring it out.

I am comfortable with programming in general, I am interested in the remarks you suggested in https://github.com/JacquesLucke/animation_nodes/issues/1026#issuecomment-462007592 I'll interested in working with you on this node.

I just started following the developer's guide. I wrote the following code for a variable node for a Vector. The idea is very simple: initialize an instance Vector variable to (0,0,0). And then update it if the Write socket is enabled.

But I will the following error: AttributeError: 'VarVector' object has no attribute 'value_cache'

I originally use value_cache as the name of the instance variable (want it to be private), then the error was: AttributeError: 'VarVector' object has no attribute '_VarVectorvalue_cache'

I try declare value_cache inside init, it still doesn't work. If I declare the variable outside create(), make it a class variable; then there is no compilation error. But the behavior of the node will be wrong.

import bpy
from mathutils import Vector
from .. base_types import AnimationNode

class VarVector(bpy.types.Node, AnimationNode):
  bl_idname = "an_VarVector"
  bl_label = "A container for storing and retrieving a Vector variable"

  def create(self):
    self.value_cache = Vector((0,0,0))     # instance variable for storing/retrieving a Vector value
    self.newInput("Boolean", "Write", "do_write")
    self.newInput("Vector", "Value In", "value_in")
    self.newOutput("Vector", "Value Out", "value_out")

  def execute(self, do_write, value_in):
    if do_write:
      self.value_cache = value_in
    return self.value_cache

I am sure I miss something simple in the code.

The idea in the following picture is very simple: write to the green vector variable node every 15 frames. Read the vector variable always. The vector variable is used to set the position of a cube. Correct behavior is the cube should jump 15 meters (instead of smooth move 1 meter per frame) every 15 frames.

image

Clockmender commented 5 years ago

Nah! You don't do it like that!

See this image:

screen shot 2019-02-11 at 15 17 56

You declare the variables in the main definition and then process them through the "Mode" settings. Below is the code for this node so far, I will look at adding Rotation Eulers/ Quaternions later on.

Here is the code for this new node (Tried and tested in blender 2.79 and AN 2.0 BTW, not on Blender 2.8 and AN 2.1 yet):

import bpy
from ... base_types import AnimationNode
from bpy.props import *
from mathutils import Vector
from ... events import propertyChanged

class variableStore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableStore"
    bl_label = "Variable Store"
    bl_width_default = 150

    strV = StringProperty()
    intV = IntProperty(default = 0)
    floV = FloatProperty(default = 0)
    vexV = FloatProperty(default = 0)
    veyV = FloatProperty(default = 0)
    vezV = FloatProperty(default = 0)
    enum = [("STRING","String","String Variable","",0),
        ("FLOAT","Float","Float Variable","",1),
        ("INTEGER","Integer","Integer Variable","",2),
        ("VECTOR","Vector","Vector Variable","",3)]

    mode = EnumProperty(name = "Type", items = enum, update = AnimationNode.refresh)

    def draw(self,layout):
        layout.prop(self, "mode")

    def create(self):
        if self.mode == "STRING":
            self.newInput("Text", "Input", "varInput")
            self.newOutput("Text", "Output", "varOutput")
        elif self.mode == "INTEGER":
            self.newInput("Integer", "Input", "varInput")
            self.newOutput("Integer", "Output", "varOutput")
        elif self.mode == "FLOAT":
            self.newInput("Float", "Input", "varInput")
            self.newOutput("Float", "Output", "varOutput")
        elif self.mode == "VECTOR":
            self.newInput("Vector", "Input", "varInput")
            self.newOutput("Vector", "Output", "varOutput")
        self.newInput("Boolean", "Process", "boolInput")

    def execute(self,varInput,boolInput):
        if self.mode == "STRING":
            if boolInput:
                self.strV = varInput
                varOutput = varInput
            else:
                varOutput = self.strV
        elif self.mode == "INTEGER":
            if boolInput:
                self.intV = varInput
                varOutput = varInput
            else:
                varOutput = self.intV
        elif self.mode == "FLOAT":
            if boolInput:
                self.floV = varInput
                varOutput = varInput
            else:
                varOutput = self.floV
        elif self.mode == "VECTOR":
            if boolInput:
                self.vexV = varInput.x
                self.veyV = varInput.y
                self.vezV = varInput.z
                varOutput = varInput
            else:
                varOutput = Vector((self.vexV,self.veyV,self.vezV))
        return varOutput

I use Atom for my code writing... This setup only changes at 15 frame intervals, it changes the Text, Integer, Float and Vector mode outputs. I will upload blend file and .py node file later.

Cheers, Clock.

Another image of the cube jumping 3m every 15 frames:

screen shot 2019-02-11 at 15 38 33

Clockmender commented 5 years ago

And here is no reason why it would not work with Quaternions:

screen shot 2019-02-11 at 16 13 40

But I m not sure where you would use this...

The Node Code now:

import bpy
from ... base_types import AnimationNode
from bpy.props import *
from mathutils import Vector, Euler, Quaternion
from ... events import propertyChanged

class variableStore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableStore"
    bl_label = "Variable Store"
    bl_width_default = 150

    strV = StringProperty()
    intV = IntProperty(default = 0)
    floV = FloatProperty(default = 0)
    vexV = FloatProperty(default = 0)
    veyV = FloatProperty(default = 0)
    vezV = FloatProperty(default = 0)
    vewV = FloatProperty(default = 0)
    enum = [("STRING","String","String Variable","",0),
        ("FLOAT","Float","Float Variable","",1),
        ("INTEGER","Integer","Integer Variable","",2),
        ("VECTOR","Vector","Vector Variable","",3),
        ("EULER","Euler","Euler Rotation Variable","",4),
        ("QUATERNION","Quaternion","Quaternion Rotation Variable","",5)]

    mode = EnumProperty(name = "Type", items = enum, update = AnimationNode.refresh)

    def draw(self,layout):
        layout.prop(self, "mode")

    def create(self):
        if self.mode == "STRING":
            self.newInput("Text", "Input", "varInput")
            self.newOutput("Text", "Output", "varOutput")
        elif self.mode == "INTEGER":
            self.newInput("Integer", "Input", "varInput")
            self.newOutput("Integer", "Output", "varOutput")
        elif self.mode == "FLOAT":
            self.newInput("Float", "Input", "varInput")
            self.newOutput("Float", "Output", "varOutput")
        elif self.mode == "VECTOR":
            self.newInput("Vector", "Input", "varInput")
            self.newOutput("Vector", "Output", "varOutput")
        elif self.mode == "EULER":
            self.newInput("Euler", "Input", "varInput")
            self.newOutput("Euler", "Output", "varOutput")
        elif self.mode == "QUATERNION":
            self.newInput("Quaternion", "Input", "varInput")
            self.newOutput("Quaternion", "Output", "varOutput")
        self.newInput("Boolean", "Process", "boolInput")

    def execute(self,varInput,boolInput):
        if self.mode == "STRING":
            if boolInput:
                self.strV = varInput
                varOutput = varInput
            else:
                varOutput = self.strV
        elif self.mode == "INTEGER":
            if boolInput:
                self.intV = varInput
                varOutput = varInput
            else:
                varOutput = self.intV
        elif self.mode == "FLOAT":
            if boolInput:
                self.floV = varInput
                varOutput = varInput
            else:
                varOutput = self.floV
        elif self.mode == "VECTOR":
            if boolInput:
                self.vexV = varInput.x
                self.veyV = varInput.y
                self.vezV = varInput.z
                varOutput = varInput
            else:
                varOutput = Vector((self.vexV,self.veyV,self.vezV))
        elif self.mode == "EULER":
            if boolInput:
                self.vexV = varInput.x
                self.veyV = varInput.y
                self.vezV = varInput.z
                varOutput = varInput
            else:
                varOutput = Euler((self.vexV,self.veyV,self.vezV))
        elif self.mode == "QUATERNION":
            if boolInput:
                self.vewV = varInput.w
                self.vexV = varInput.x
                self.veyV = varInput.y
                self.vezV = varInput.z
                varOutput = varInput
            else:
                varOutput = Quaternion((self.vewV,self.vexV,self.veyV,self.vezV))
        return varOutput

Cheers, Clock.

EDIT:

Could use it with my own "Bone Transform" Node I suppose:

screen shot 2019-02-11 at 16 32 56

Clockmender commented 5 years ago

A good idea would be to open a new issue with clear description of your proposal. That way Jacques can read it more clearly and give input.

@OmarSquircleArt See my 2 posts above my friend, I think this is what he wanted and requires no real wizardry, perhaps you, or Jacques could comment on my strange behaviour with your nodes!!!

Cheers, Clock.

mathlusiverse commented 5 years ago

@Clockmender Thanks. It works in Blender 2.8 beautifully!

The cube jumps 15 meters every 15 frames as expected!

The green node is your new Variable Store node.

image

It did gives the following un-harmful warning message on the console. May be new requirement for 2.8

image

The warning is: Warning: class an_variableStore contains a properties which should be an annotation!

Clockmender commented 5 years ago

I have no idea why it does that! I am not using 2.8 and AN 2.1 yet for anything other than playing and my 2.8 is well out of date... Perhaps @OmarSquircleArt could comment on how this might be better done in 2.8, I have not written any nodes for 2.8/AN 2.1 and won't until it is more stable, i.e. officially released! But then I'm a bit old fashioned...

Cheers, Clock.

mathlusiverse commented 5 years ago

I am testing the second version with Euler. It has the following error. I think it is just an oversight only.

varOutput = Euler((self.vexV,self.veyV,self.vezV)) NameError: name 'Euler' is not defined

Clockmender commented 5 years ago

I think you may not have altered this line:

from mathutils import Vector, Euler, Quaternion

Not sure as it works on my system. Is Euler recognised in Mathutils in Blender 2.8?

Clockmender commented 5 years ago

variable.py.zip

Try just replacing your file with this one, maybe a typo....

I now have a error I did not have before, leave it with me a little while.

mathlusiverse commented 5 years ago

It works beautifully in Blender 2.8 again. There was some hasty copy-and-paste on my part, hence some confusion. This is based on your last code (variable.py.zip).

image

Just start with a new Blender session, scale the default cube 10x and attach the default cube as shown. Then the cube jumps 15 meters per 15 frames. And rotate 20 degrees about z-axis every 20 frames. :)

To see the animation better: Scale the cube 10x, and change animation length to 100.

Clockmender commented 5 years ago

Glad it works! The error on mine was that I thought I had copied the file to my desktop to make the zip with, but I had just dragged it so it was missing from my nodes directory - what a plonker!

All is well again, maybe I should get some sleep after my 14 hour flight from Vietnam....

mathlusiverse commented 5 years ago

Thanks for your help! It is quite rewarding for me. Have a good night.

Clockmender commented 5 years ago

Thank you!

Can you think of anything else we might add as another variable type, let me know tomorrow, if you think of anything, I am off for some much needed sleep!

Also do you think it worthwhile making a node to trigger these events rather than using the Expression node, that may be beyond some people to understand and it is really easy to parcel that bit up nicely.

OmarEmaraDev commented 5 years ago

Blender 2.8 required properties to be defined as type annotations, so instead of writing str = StringProperty() you write str: StringProperty(). This should get rid of the warning.

For the implementation itself, you can use a DataTypeSelectorSocket instead of an enum to define the type. Moreover, you can use a global dictionary with the node identifiers as keys. So a quick implementation should be something like this:

import bpy
from ... base_types import AnimationNode, DataTypeSelectorSocket

variables = {}

class VariableNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_VariableNode"
    bl_label = "Variable"

    assignedType: DataTypeSelectorSocket.newProperty(default = "Float")

    def create(self):
        self.newInput("an_BooleanSocket", "Condition", "condition")
        self.newInput(DataTypeSelectorSocket("Input", "input", "assignedType"))
        self.newOutput(DataTypeSelectorSocket("Output", "output", "assignedType"))

    def execute(self, condition, input):
        if condition:
            variables[self.identifier] = input

        if self.identifier in variables and type(variables[self.identifier]) == type(input):
            return variables[self.identifier]
        else:
            return self.inputs[1].getDefaultValue()
Clockmender commented 5 years ago

Thanks Omar! I clearly have some learning to do before I port my nodes to 2.8/AN 2.1...

@mathlusiverse Here is a "Trigger" node:

import bpy
from ... base_types import AnimationNode
from bpy.props import *
from ... events import propertyChanged

class triggerNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_triggerNode"
    bl_label = "Trigger Events"
    bl_width_default = 150

    mess = StringProperty()

    def draw(self,layout):
        if self.mess != '':
            layout.label(self.mess, icon = 'ERROR')

    def create(self):
        self.newInput("Integer", "Step (Frames)", "range")
        self.newInput("Integer", "Offset", "offset")
        self.newOutput("Boolean", "Condition", "condition")

    def execute(self, range, offset):
        if offset >= range:
            offset = 0
            self.mess = 'Offset Invalid, Using 0'
        else:
            self.mess = ''
        if range > 0:
            return bpy.context.scene.frame_current % range == offset
        else:
            self.mess = 'No Step Set'
            return True

And how to use it:

screen shot 2019-02-11 at 18 38 04

Just quick and dirty for now, but the Steps is the frame interval to activate, say every 20 frames and the offset is to adjust when it triggers in the "step" range, so 20 and 5 would trigger every 20 frames starting at frame 5, being input sockets you can of course animate them with other nodes...

Now I am really off to sleep!

mathlusiverse commented 5 years ago

The following example uses 4 @Clockmender's Variable Store (orange) nodes and 1 Trigger Event (green) node. The trigger event fires once every 20 frames. A teapot will be randomly dancing, moving and changing its color.

A random number is generated and used to calculate the values of 4 variables to control the teapot's position, rotation, scale and its color.

image

3 Remarks on the code of the Trigger Event:

Blender 2.8 gives 1 warning. Solved easily by including 'text' in the following: layout.label(text = self.mess, icon = 'ERROR')

I change the input 'Frame (Step)' to 'Cycle Length', and require it to be positive.

Cycle length must be at least one. I do not impose any condition on 'Offset'. I always think the "offset' as phase. The following code allow 'Offset' to change continuously from -99999 to 99999 and still behave seamlessly:

    offset %= cycleLen
    if offset < 0:
      offset += cycleLen

Now that the variable node works, can we enhance it so that multiple instance of same-named-variable- nodes of the SAME variable can be used in different parts of the tree. The reason I ask this is because I want to be able to write/read the same variable (say, matColor) in multiple parts of the tree. Kind of like there is a 'hidden static list' storing the property matColor, so it can be referenced by different instance of the same-named-variable-node.

This feature will be useful, for example, to code x=3x+4x*(2-x) where x is an integer variable: read x, calculate new x, and then write new value back to x again.

Side note: The above example is actually a very special case, and can be achieved by new Update Expression node: get its own value in the input, and calculate it, and automatically update its own value, and produce the new output as bonus.

Clockmender commented 5 years ago

I see the Variable node has changed a little as well, can you post the full code for both nodes as you have them now, so I can update my versions to match yours, I will adjust for blender 2.79/AN 2.0. I also see you have a Boolean option for the Variable node.

Now that the variable node works, can we enhance it so that multiple instance of same-named-variable- nodes of the SAME variable can be used in different parts of the tree. The reason I ask this is because I want to be able to write/read the same variable (say, matColor) in multiple parts of the tree. Kind of like there is a 'hidden static list' storing the property matColor, so it can be referenced by different instance of the same-named-variable-node.

Won't this cause a conflict between multiple nodes trying to set the same thing?

Thanks, Clock.

EDIT:

You can control the input socket values like this as an alternative:

def create(self):
        self.newInput("Integer", "Cycle Length", "cycLen", minValue = 1)
        self.newInput("Integer", "Offset", "offset", minValue = 0)

Note it is minValue and maxValue, or course rather than min and max as it is for Properties...

Clockmender commented 5 years ago

I have re-created your setup in Blender 2.79 / AN 2.0:

screen shot 2019-02-12 at 10 04 31

I will give some thought to your idea changing the variably with different nodes, I think there're ways to do this already (with Booleans you can just multiply various ones together in an Expression node).

Cheers, Clock.

EDIT:

I thought about the offset - cycLen thing again and this works nicely and auto-corrects:

import bpy
from ... base_types import AnimationNode
from bpy.props import *
from ... events import propertyChanged

class triggerNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_triggerNode"
    bl_label = "Periodic Trigger"
    bl_width_default = 150

    def create(self):
        self.newInput("Integer", "Cycle Length", "cycLen", minValue = 1)
        self.newInput("Integer", "Offset", "offset", minvalue = 0)
        self.newOutput("Boolean", "Condition", "condition")

    def execute(self, cycLen, offset):
        if offset >= cycLen:
            offset = offset % cycLen
        return bpy.context.scene.frame_current % cycLen == offset

This changes at frame 6 as it should:

screen shot 2019-02-12 at 10 38 01

mathlusiverse commented 5 years ago

Here is the code for Variable Store (Blender 2.8 beta, AN 2.1.3)

import bpy
from bpy.props import *
from mathutils import Vector, Euler, Quaternion
from .. base_types import AnimationNode
from .. events import propertyChanged

class VariableStore_Idea(bpy.types.Node, AnimationNode):
  bl_idname = "an_VariableStore_Idea"
  bl_label = "Store for a variable"

  varStr : StringProperty()
  varInt : IntProperty()
  varBoo : BoolProperty(default = False)
  varFx  : FloatProperty() # shared by Vector.x and Euler.x
  varFy  : FloatProperty() # shared by Vector.y and Euler.y
  varFz  : FloatProperty() # shared by Vector.z and Euler.z
  varFw  : FloatProperty() # shared by Float    and Euler.w

  varTypeEnum = [("STRING",     "String",     "String Variable",              "",0),
                 ("FLOAT",      "Float",      "Float Variable",               "",1),
                 ("INTEGER",    "Integer",    "Integer Variable",             "",2),
                 ("BOOLEAN",    "Boolean",    "Boolean Variable",             "",3),
                 ("VECTOR",     "Vector",     "Vector Variable",              "",4),
                 ("EULER",      "Euler",      "Euler Rotation Variable",      "",5),
                 ("QUATERNION", "Quaternion", "Quaternion Rotation Variable", "",6)]

  varType : EnumProperty(name = "Variable Type", items = varTypeEnum, 
                         default = "FLOAT",  update = AnimationNode.refresh)

  def draw(self,layout):
    layout.prop(self, "varType")

  def create(self):
    self.newInput("Boolean", "Store", "boolInput")
    if self.varType == "STRING":
      self.newInput("Text", "Input", "varInput")
      self.newOutput("Text", "Output", "varOutput")
    elif self.varType == "INTEGER":
      self.newInput("Integer", "Input", "varInput")
      self.newOutput("Integer", "Output", "varOutput")
    elif self.varType == "FLOAT":
      self.newInput("Float", "Input", "varInput")
      self.newOutput("Float", "Output", "varOutput")
    elif self.varType == "BOOLEAN":
      self.newInput("Boolean", "Input", "varInput")
      self.newOutput("Boolean", "Output", "varOutput")
    elif self.varType == "VECTOR":
      self.newInput("Vector", "Input", "varInput")
      self.newOutput("Vector", "Output", "varOutput")
    elif self.varType == "EULER":
      self.newInput("Euler", "Input", "varInput")
      self.newOutput("Euler", "Output", "varOutput")
    elif self.varType == "QUATERNION":
      self.newInput("Quaternion", "Input", "varInput")
      self.newOutput("Quaternion", "Output", "varOutput")

  def execute(self, boolInput, varInput):
    if self.varType == "STRING":
      if boolInput:
        self.varStr = varInput
        varOutput = varInput
      else:
        varOutput = self.varStr
    elif self.varType == "INTEGER":
      if boolInput:
        self.varInt = varInput
        varOutput = varInput
      else:
        varOutput = self.varInt
    elif self.varType == "FLOAT":
      if boolInput:
        self.varFw = varInput
        varOutput = varInput
      else:
        varOutput = self.varFw
    elif self.varType == "BOOLEAN":
      if boolInput:
        self.varBoo = varInput
        varOutput = varInput
      else:
        varOutput = self.varBoo
    elif self.varType == "VECTOR":
      if boolInput:
        self.varFx = varInput.x
        self.varFy = varInput.y
        self.varFz = varInput.z
        varOutput = varInput
      else:
        varOutput = Vector((self.varFx, self.varFy, self.varFz))
    elif self.varType == "EULER":
      if boolInput:
        self.varFx = varInput.x
        self.varFy = varInput.y
        self.varFz = varInput.z
        varOutput = varInput
      else:
        varOutput = Euler((self.varFx, self.varFy, self.varFz))
    elif self.varType == "QUATERNION":
      if boolInput:
        self.varFw = varInput.w
        self.varFx = varInput.x
        self.varFy = varInput.y
        self.varFz = varInput.z
        varOutput = varInput
      else:
        varOutput = Quaternion((self.varFw, self.varFx, self.varFy, self.varFz))
    return varOutput

Here is the code for Periodic Trigger Event (Blender 2.8 beta, AN 2.1.3)

import bpy
from bpy.props import *
from .. base_types import AnimationNode
from .. events import propertyChanged

class PeriodicTriggerEvent(bpy.types.Node, AnimationNode):
  bl_idname = "an_PeriodicTriggerEvent"
  bl_label = "Fire a trigger signal periodically"

  errMsg : StringProperty()

  def create(self):
    self.newInput("Integer", "Period", "period")
    self.newInput("Integer", "Phase", "phase")
    self.newOutput("Boolean", "Trigger", "trigger")

  def execute(self, period, phase):
    # period must be positive
    if period <= 0:
      self.errMsg = "Period must be positive"
      return False
    # everything is OK now
    self.errMsg = ""
    # no restriction on phase
    # eg: make sure phase=...,-22,-12,-2,8,18,28,... are the same for period=10
    phase %= period
    if phase < 0:
      phase += period
    return bpy.context.scene.frame_current % period == phase

  def draw(self,layout):
    if self.errMsg != "":
      layout.label(text = self.errMsg, icon = "ERROR")

Let me explain what I meant by being able to use multiple same-named Variable Nodes in different places in the tree. For example, I have a variable named "speed". Following pictures are not real: the "Name" field is fake!

image

Now I can use the speed to control an object's position. When I use it to read the value, I can hide sockets related to writing (this can currently be done). It would be less confusing to display "Output" as "Value" in this case.

image

I can collapse the variable node. The collapsed Variable Node display "Speed" (this can currently be done).

image

If I want to use the above logic to control different objects. Current implementation of variable node require me to read the speed value from a single variable node. That means if I have 10 such objects in different corners of the tree, then I will have ten lines running all over in the tree.

My idea is that the "speed" variable node can be used in different places just like "Time info (frame)".

Clockmender commented 5 years ago

Aha! I understand, the problem here is that the variable in the node is not accessible from outside the node, so that won't work. But this might, let me try it and get back to you:

Introduce a Custom Property somewhere in the blend file and set it to the variable, then read this with a node - might work, let me think about it.