JacquesLucke / animation_nodes

Node based visual scripting system designed for motion graphics in Blender.
Other
2.29k 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.

Clockmender commented 5 years ago

This works:

screen shot 2019-02-12 at 17 03 45

Maybe I can knock up a quick pair of nodes to do this easily.

Clockmender commented 5 years ago

This works:

screen shot 2019-02-12 at 18 28 26

It makes the CP's if they don't exist and I seem to be able to store anything in them, sometimes need a converter to get it back to what it was before though.

Code for the nodes:

Writer:

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

class writeCPNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_writeCPNode"
    bl_label = "Store CP to Object"

    def create(self):
        self.newInput("Object", "Object", "obj")
        self.newInput("Text", "CP Name", "cpName")
        self.newInput("Generic", "Input", "inpV")

    def execute(self, obj, cpName, inpV):
        obj[cpName] = inpV

Reader:

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

class readCPNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_readCPNode"
    bl_label = "Read CP from Object"

    def create(self):
        self.newInput("Object", "Object", "obj")
        self.newInput("Text", "CP Name", "cpName")
        self.newOutput("Generic", "Output", "outV")

    def execute(self, obj, cpName):
        return obj[cpName]

Just needs a simple object to store the CP's, I used an Empty and I think you can have a shed load of CP's on it. Let me know what you think, you can put any number of the Readers anywhere on your node tree.

Thanks for the Nodes you are using for 2.8 BTW.

EDIT:

Another example of usage, would be more use in a really complex node tree :-)

screen shot 2019-02-12 at 19 00 48

mathlusiverse commented 5 years ago

The CustomPropertyWriter and CustomPropertyReader nodes work! :)

I add an empty object, a cube and a teapot in the scene. Define two variables: PositionX (float) and Orientation (Euler). Store them as custom property of the empty object. Update the value of PositionX and Orientation periodically. Read the values of the two variables by cube and teapot at each frame. Behavior is as expected.

image

I have to do a simple modification to the Writer and Reader nodes, otherwise dropping a new Writer and Reader node will cause error as the 'backing object' is undefined at that time.

def execute(self, obj, cpName, inpV):   # writer
   if obj != None:
     obj[cpName] = inpV

def execute(self, obj, cpName):   # reader
   if obj != None:
     return obj[cpName]
   return None

Thanks @Clockmender! Wonder if we can 'hide' the Variable Store and Convert nodes inside the Writer and Reader node. I will play with this a little bit more. Just want to let you know that the Writer and reader nodes work as it is!

EDIT: I tested the Read/Write nodes with storing two custom properties of different types (float and Euler). So we should be able to use one single empty object to store any number of variables. It will be nice if the node implicitly create a new empty object the very first time a user use a variable node. That way the user does not have to know anything about the custom property and the magic empty object. I don't know how to do it yet.

Clockmender commented 5 years ago

@mathlusiverse Thanks for that, I tried to build the converter in but ran against a lack of knowledge on my behalf. Getting the First Variable node to add the Empty is doable, I will look at that in a moment, however, I need some help from Omar first..

@OmarSquircleArt Hello Sir! I have a little problem, I a trying to build a data type converter into the read node for this system, but have hit an impasse, here is my setup:

screen shot 2019-02-13 at 13 06 47

And here is my code to date, including notes at the bottom on the bit that doesn't work:

import bpy
from bpy.props import *
from ... base_types import AnimationNode, AutoSelectDataType
from ... sockets.info import toIdName

class CPConvertNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_CPConvertNode"
    bl_label = "Get CP & Convert"
    bl_width = 100

    dataType = StringProperty(default = "Generic", update = AnimationNode.refresh)
    lastCorrectionType = IntProperty()

    fixedOutputDataType = BoolProperty(name = "Fixed Data Type", default = False,
        description = "When activated the output type does not automatically change",
        update = AnimationNode.refresh)

    def create(self):
        self.newInput("Object", "Object", "Object", "obj")
        self.newInput("Text", "CP Name", "cpName")
        self.newOutput(self.dataType, "New", "new")

        if not self.fixedOutputDataType:
            self.newSocketEffect(AutoSelectDataType(
                "dataType", [self.outputs[0]], ignore = {"Generic"}))

    def draw(self, layout):
        row = layout.row(align = True)
        self.invokeSelector(row, "DATA_TYPE", "assignOutputType", text = "to " + self.dataType)
        icon = "LOCKED" if self.fixedOutputDataType else "UNLOCKED"
        row.prop(self, "fixedOutputDataType", icon = icon, text = "")

        if self.lastCorrectionType == 2:
            layout.label("Conversion Failed", icon = "ERROR")

    def assignOutputType(self, dataType):
        self.fixedOutputDataType = True
        if self.dataType != dataType:
            self.dataType = dataType

    def getExecutionCode(self):
        # Test Value for cpVal - this works
        cpVal = '20.76'
        # What I want is the value for obj[cpName] to convert that to another type
        # so something like this:
        #cpVal = obj[cpName]
        # but this doesn't work...
        yield "new, self.lastCorrectionType = self.outputs[0].correctValue("+cpVal+")"

Can you point me in the right direction please? I just need to get the value of the Custom Property named in cpName converted to the chosen data type.

Cheers, Clock.

Clockmender commented 5 years ago

@mathlusiverse

Here is the node that does the Custom Property store onto an empty, creates the empty if it doesn't exist and writes the value to it. I need to check for over-writing an existing CP, off flying now for an hour.

screen shot 2019-02-13 at 15 05 11

And the code for 2.79 / AN 2.0:

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

class variableCPStore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableCPStore"
    bl_label = "Variable CP Store"
    bl_width_default = 200

    strV = StringProperty()
    booV = BoolProperty(default = True)
    mess = 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),
        ("BOOLEAN","Boolean","Boolean Rotation Variable","",6)]

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

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

    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")
        elif self.mode == "BOOLEAN":
            self.newInput("Boolean", "Input", "varInput")
            self.newOutput("Boolean", "Output", "varOutput")
        self.newInput("Boolean", "Process", "boolInput")
        self.newInput("Text", "CP Name", "cpName")

    def execute(self,varInput,boolInput,cpName):
        if cpName == '':
            self.mess = 'Enter CP Name'
            return None
        else:
            self.mess = ''
        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 == "BOOLEAN":
            if boolInput:
                self.booV = varInput
                varOutput = varInput
            else:
                varOutput = self.booV
        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))
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj == None:
            bpy.ops.object.add(type='EMPTY',location=(0,0,0),radius = 0.3)
            bpy.context.active_object.name = 'CP_Empty'
            bpy.context.active_object.empty_draw_type = "SINGLE_ARROW"
            bpy.context.active_object.show_name = True
            bpy.context.active_object.layers[19] = True
            for i in range(18):
                bpy.context.active_object.layers[i] = False
            bpy.context.active_object.select = False
            cpObj = bpy.data.objects.get('CP_Empty')
        cpObj[cpName] = varOutput

        return varOutput

Let me know what you think to this please, it puts the empty at 0,0,0 on layer 19. If you delete the Empty, it automatically gets re-created next execution of the node tree, with all CPs assigned by nodes BTW.

Cheers, Clock.

OmarEmaraDev commented 5 years ago

@Clockmender Hi, I am not entirely sure what you want to do, but:

  1. getExecutionCode method should take two parameters, self and required. You only provided self.
  2. cpVal = obj[cpName] don't work for two reasons:
    • obj and cpName are not defined, those are variables that AN will define/fill in the execution code, but they do not exist in this namespace.
    • Aside from the fact that it doesn't exist due to the aforementioned point. cpVal is an object and not necessarily a string. You are trying to concatenate a string with an object.

So the correct implementation should be something like this:

def getExecutionCode(self, required):
    return "new, self.lastCorrectionType = self.outputs[0].correctValue(obj[cpName])"
Clockmender commented 5 years ago

@OmarSquircleArt Thanks for the comment above, I will try to re-work this tomorrow, far too tired and jet lagged now! I appreciate your help. I am trying to convert what is in a Custom Property to another type, like a Vector, or Euler, etc. I have found another way to do this, but it is not as good as this would be if I got it working. Basically I tried to borrow the code from the convert node, but did not fully understand it all - it had no "required" bit in it so that's where that error came from, I learn more every day!

@mathlusiverse Update:

screen shot 2019-02-13 at 19 33 10

Here's the code for the new "Reader" node - it takes the values form the CP_Empty object and outputs them for animations:

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

class CPConvertNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_CPConvertNode"
    bl_label = "Get CP & Convert"
    bl_width = 100

    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),
        ("BOOLEAN","Boolean","Boolean Rotation Variable","",6)]

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

    def create(self):
        self.newInput("Text","CP Name", "cpName")
        if self.mode == "STRING":
            self.newOutput("Text", "Output", "varOutput")
        elif self.mode == "INTEGER":
            self.newOutput("Integer", "Output", "varOutput")
        elif self.mode == "FLOAT":
            self.newOutput("Float", "Output", "varOutput")
        elif self.mode == "VECTOR":
            self.newOutput("Vector", "Output", "varOutput")
        elif self.mode == "EULER":
            self.newOutput("Euler", "Output", "varOutput")
        elif self.mode == "QUATERNION":
            self.newOutput("Quaternion", "Output", "varOutput")
        elif self.mode == "BOOLEAN":
            self.newOutput("Boolean", "Output", "varOutput")

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

    def execute(self,cpName):
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj != None:
            cps = cpObj.keys()
            if cpName in cps:
                if self.mode == 'STRING':
                    return str(cpObj[cpName])
                elif self.mode == 'INTEGER':
                    if type(cpObj[cpName]).__name__ == 'int':
                        self.mess = ''
                        return int(cpObj[cpName])
                    else:
                        self.mess = 'CP is '+type(cpObj[cpName]).__name__
                        return None
                elif self.mode == 'FLOAT':
                    if type(cpObj[cpName]).__name__ == 'float':
                        self.mess = ''
                        return float(cpObj[cpName])
                    else:
                        self.mess = 'CP is '+type(cpObj[cpName]).__name__
                        return None
                elif self.mode == 'VECTOR':
                    if type(cpObj[cpName]).__name__ == 'IDPropertyArray':
                        self.mess = ''
                        return Vector(cpObj[cpName])
                    else:
                        self.mess = 'CP is '+type(cpObj[cpName]).__name__
                        return None
                elif self.mode == 'EULER':
                    if type(cpObj[cpName]).__name__ == 'IDPropertyArray':
                        self.mess = ''
                        return Euler(cpObj[cpName])
                    else:
                        self.mess = 'CP is '+type(cpObj[cpName]).__name__
                        return None
                elif self.mode == 'QUATERNION':
                    if type(cpObj[cpName]).__name__ == 'IDPropertyArray':
                        self.mess = ''
                        return Quaternion(cpObj[cpName])
                    else:
                        self.mess = 'CP is '+type(cpObj[cpName]).__name__
                        return None
                elif self.mode == 'BOOLEAN':
                    if type(cpObj[cpName]).__name__ == 'int':
                        self.mess = ''
                        return bool(cpObj[cpName])
                    else:
                        self.mess = 'CP is '+type(cpObj[cpName]).__name__
                        return None
            else:
                return None

And this one just lists the Custom Property names from CP_Empty, (useful if you don't want to keep finding the CP_Empty object):

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

class getCPfromObj(bpy.types.Node, AnimationNode):
    bl_idname = "getCPfromObj"
    bl_label = "Get Custom Properties"
    bl_width = 220

    mess = StringProperty()

    def create(self):
        self.newOutput("Text List", "Custom Properties", "cusProp")

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

    def execute(self):
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj != None:
            self.mess = ''
            return cpObj.keys()
        else:
            self.mess = 'CP_Empty Not Found'
            return None

I am going to try to get my original thought working after help from Omar, but not until tomorrow now...

Cheers, Clock.

OmarEmaraDev commented 5 years ago

@Clockmender Oh, I keep forgetting that you are on an old version. You don't need the required parameter if you are not on v2.1+ versions. The other points still stands though.

Clockmender commented 5 years ago

@OmarSquircleArt

This is the code now:

import bpy
from bpy.props import *
from ... base_types import AnimationNode, AutoSelectDataType
from ... sockets.info import toIdName

class objCPConvertNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_objCPConvertNode"
    bl_label = "Get Object CP & Convert"
    bl_width = 100

    dataType = StringProperty(default = "Generic", update = AnimationNode.refresh)
    lastCorrectionType = IntProperty()

    fixedOutputDataType = BoolProperty(name = "Fixed Data Type", default = False,
        description = "When activated the output type does not automatically change",
        update = AnimationNode.refresh)

    def create(self):
        self.newInput("Object", "Object", "Object", "obj")
        self.newInput("Text", "CP Name", "cpName")
        self.newOutput(self.dataType, "New", "new")

        if not self.fixedOutputDataType:
            self.newSocketEffect(AutoSelectDataType(
                "dataType", [self.outputs[0]], ignore = {"Generic"}))

    def draw(self, layout):
        row = layout.row(align = True)
        self.invokeSelector(row, "DATA_TYPE", "assignOutputType", text = "to " + self.dataType)
        icon = "LOCKED" if self.fixedOutputDataType else "UNLOCKED"
        row.prop(self, "fixedOutputDataType", icon = icon, text = "")

        if self.lastCorrectionType == 2:
            layout.label("Conversion Failed", icon = "ERROR")

    def assignOutputType(self, dataType):
        self.fixedOutputDataType = True
        if self.dataType != dataType:
            self.dataType = dataType

    def getExecutionCode(self):
        return "new, self.lastCorrectionType = self.outputs[0].correctValue(obj[cpName])"

This doesn't work, error message:

import sys, bpy import itertools from time import perf_counter as getCurrentTime from mathutils import Vector, Matrix, Quaternion, Euler AN = animation_nodes = sys.modules.get('animation_nodes') from animation_nodes.data_structures import * from animation_nodes import algorithms _node_execution_times = animation_nodes.execution.measurements.getMeasurementsDict() nodes = bpy.data.node_groups['NodeTree'].nodes _reovxhh4uny26zv = nodes['Get Object CP & Convert'] _yxvbd5lhpauuk0p = nodes['Viewer'] _Object_reo0 = _reovxhh4uny26zv.inputs[0].getValue() _cpName_reo1 = _reovxhh4uny26zv.inputs[1].getValue()

Node: 'NodeTree' - 'Get Object CP & Convert'

_new_reo2, _reovxhh4uny26zv.lastCorrectionType = _reovxhh4uny26zv.outputs[0].correctValue(obj[_cpName_reo1])

Node: 'NodeTree' - 'Viewer'

_yxvbd5lhpauuk0p.execute(_new_reo2)

obj is the input object variable, cpName is the Custom Property name.

Blender Window:

screen shot 2019-02-14 at 08 07 36

I am not sure where I go from here - Blender 2.79 AN 2.0 - both official releases.

EDIT:

This as the last line and get rid of the object input:

def getExecutionCode(self):
        return "new, self.lastCorrectionType = self.outputs[0].correctValue(bpy.data.objects.get('CP_Empty')[cpName])"

DOES work, if I call the object specifically - I am confused, clearly it doesn't like the obj input....

screen shot 2019-02-14 at 08 40 24

OmarEmaraDev commented 5 years ago

@Clockmender The problem is in this line:

self.newInput("Object", "Object", "Object", "obj")

It should be:

self.newInput("Object", "Object", "obj")
Clockmender commented 5 years ago

@OmarSquircleArt

Now I feel really silly, it is amazing how many times one can look at code and not see basic errors!

Thank you so much. All is good now. Although I don't seem to be able to test for obj having a value, like this:

if obj != None:
   # Do somehting...
OmarEmaraDev commented 5 years ago

@Clockmender What exactly is the problem? Can you show me the full code?

A better way to check for None objects is to use if obj is not None:

Clockmender commented 5 years ago

So I wanted to check that obj input and cpName is not None. My execution code is therefore:

def getExecutionCode(self):
    if obj is not None and cpName is not None:
        return "new, self.lastCorrectionType = self.outputs[0].correctValue(obj[cpName])"
    else:
        return ''

But this always causes an error, I think you said that obj and cpName are not available in this namespace, so I don't think I can perform this check? it is not that important, it just means that you get the red outline until you have entered the values.

OmarEmaraDev commented 5 years ago

When using getExecutionCode method instead of the more conventional execute function. You return a string (or an iterator of strings) that will be embedded directly into the execution code of the node tree. Animation Nodes will replace the inputs (obj, cpName) and the self to the appropriate objects.

So to solve this problem, simply include your code in the string directly like this:

def getExecutionCode(self):
    yield "if obj is not None and cpName is not None:"
    yield "    new, self.lastCorrectionType = self.outputs[0].correctValue(obj[cpName])"
    yield "else:"
    yield "    new = whatever"

Here is the thing, you don't really need to use the getExecutionCode function here, just use execute to avoid all of these problems.

Clockmender commented 5 years ago

OK Leave that with me for a while, I need to go out for about an hour or so.

Can I use return "new, self.lastCorrectionType = self.outputs[0].correctValue(obj[cpName])" in an execute function then? I thought not... but then I get a lot of things wrong at times :-)

OmarEmaraDev commented 5 years ago

Well no, but you can write it as a normal function like so:

def execute(self, obj, cpName):
    new, self.lastCorrectionType = self.outputs[0].correctValue(obj[cpName])
    return new
mathlusiverse commented 5 years ago

After spending many many hours debugging and learning the concepts of property and collection in Blender 2.8, I finally have the Variable Node done for the moment. Still have more work to do. I am going to post what I have so far.

It is now a single variable node that can write/read variables and used anywhere in the same tree. It does not require converter. The idea is to create an empty object automatically and store all variables as custom property of the object. The green node is the Variable Node.

image

image

image

The code is here:

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

class  MlvAn_03e(bpy.types.Node, AnimationNode):
  bl_idname = 'an_Mlv_03e'
  bl_label = 'MLV_01e: Write/read variable to a common hidden object'

  msg : StringProperty()

  varTypeEnum = [
       # (identifier, displayName, description, icon, ordinal)
       # ordinal: unique constant
#      ('STRING',     'String',     'String type variable',                    '',0),
       ('FLOAT',      'Float',      'Float type variable',                     '',1),
       ('INTEGER',    'Integer',    'Integer type variable',                   '',2),
       ('BOOLEAN',    'Boolean',    'Boolean type variable',                   '',3),
       ('VECTOR',     'Vector',     'Vector type variable',                    '',4),
       ('EULER',      'Euler',      'Euler (for rotation) type variable',      '',5),
       ('QUATERNION', 'Quaternion', 'Quaternion (for rotation) type variable', '',6) ]

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

  def draw(self,layout):
    layout.prop(self, 'varType')
    if self.msg != '':
      layout.label(text = self.msg, icon = 'ERROR')

  def create(self):
    self.val = None
    # newInOutput( Type, Name, Identifier )
    self.newInput('Text', 'Name', 'cpName')
    if self.varType == 'STRING':
      self.newInput( 'Text', 'New Value', 'valNew')
      self.newOutput('Text', 'Value', 'valueCurr')
    elif self.varType == 'INTEGER':
      self.newInput( 'Integer', 'New Value', 'valNew')
      self.newOutput('Integer', 'Value', 'valueOut')
    elif self.varType == 'FLOAT':
      self.newInput( 'Float', 'New Value', 'valNew')
      self.newOutput('Float', 'Value', 'valueOut')
    elif self.varType == 'BOOLEAN':
      self.newInput( 'Boolean', 'New Value', 'valNew')
      self.newOutput('Boolean', 'Value', 'valueOut')
    elif self.varType == 'VECTOR':
      self.newInput( 'Vector', 'New Value', 'valNew')
      self.newOutput('Vector', 'Value', 'valueOut')
    elif self.varType == 'EULER':
      self.newInput( 'Euler', 'New Value', 'valNew')
      self.newOutput('Euler', 'Value', 'valueOut')
    elif self.varType == 'QUATERNION':
      self.newInput( 'Quaternion', 'New Value', 'valNew')
      self.newOutput('Quaternion', 'Value', 'valueOut')
    self.newInput('Boolean', 'Set Value', 'setValue')

  def defaultVal(self):
    if self.varType == 'STRING':
      return ''
    elif self.varType == 'INTEGER':
      return 0
    elif self.varType == 'FLOAT':
      return 0
    elif self.varType == 'BOOLEAN':
      return False
    elif self.varType == 'VECTOR':
      return Vector(0,0,0)
    elif self.varType == 'EULER':
      return Euler()
    elif self.varType == 'QUATERNION':
      return Quaternion()

  def execute(self, cpName, valNew, setValue):
    if cpName == '':
      self.msg = 'Variable name missing'
      return self.defaultVal()
    self.msg = ''
    cpObj = self.getCpObj()
    val = self.getCpObjVal(cpObj, cpName)
    if setValue:
      cpObj[cpName] = valNew
      self.setCpObjVal(cpObj, cpName, valNew)
      return valNew
    else:
      return val

  def getCpObj(self):
    COLL_NAME = 'Animation Nodes Object Container' #  shared by all nodes
    CPOBJ_NAME = 'AnimNodeVarStore' #  shared by all nodes
    cpObj = bpy.data.objects.get(CPOBJ_NAME)
    if cpObj == None:
      cpObj = bpy.data.objects.new(name = CPOBJ_NAME, object_data = None) 
      try:  # find the existing collection
        coll = bpy.data.collections[COLL_NAME]
      except:
        pass
      if coll == None:  # not found
        # create new collection
        coll = bpy.data.collections.new(COLL_NAME)
        # link the newCol to the scene
        bpy.context.scene.collection.children.link(coll)
      bpy.data.collections[COLL_NAME].objects.link(cpObj)
    return cpObj

  def getCpObjVal(self, cpObj, cpName):
    try:
      val = cpObj[cpName]
      return val
    except:
      if self.varType == 'STRING':
        pass # not implemented yet
      elif self.varType == 'FLOAT':
        cpObj[cpName] = 0
      elif self.varType == 'INTEGER':
        cpObj[cpName] = 0
      elif self.varType == 'BOOLEAN':
        cpObj[cpName] = 0
      elif self.varType == 'VECTOR':
        cpObj[cpName] = [0,0,0]
      elif self.varType == 'EULER':
        cpObj[cpName] = [0,0,0]
      elif self.varType == 'QUATERNION':
        cpObj[cpName] = [0,0,0,1]
    val = cpObj[cpName]
    return val

  def setCpObjVal(self, cpObj, cpName, valNew):
    if self.varType == 'STRING':
      pass # not implemented yet
    elif self.varType == 'FLOAT':
      cpObj[cpName] = valNew
    elif self.varType == 'INTEGER':
      cpObj[cpName] = round(valNew)
    elif self.varType == 'BOOLEAN':
      cpObj[cpName] = 1 if valNew else 0
    elif self.varType == 'VECTOR':
      cpObj[cpName] = [valNew.x, valNew.y, valNew.z]
    elif self.varType == 'EULER':
      cpObj[cpName] = [valNew.x, valNew.y, valNew.z]
    elif self.varType == 'QUATERNION':
      cpObj[cpName] = [valNew.x, valNew.y, valNew.z, valNew.w]

To do:

  1. String variable
  2. Automatically remove property when variable name is not used anymore
  3. When user type 'Speed' as variable name, depending on the timing, following custom property will be created: 'S', 'Sp', 'Spe', 'Spee', "Speed'.
  4. Color of the Variable Node revert back to default on its own.
  5. ...
Clockmender commented 5 years ago

@mathlusiverse - Looking good but I don't think I can use this in 2.79... I see you are using Collections! I will wait for your final version, although aren't you still using a separate "read" node as well? I am getting confused just now.

@OmarSquircleArt - Sorry for being thick, I blame jet lag still, or old age.

This now works beautifully:

import bpy
from bpy.props import *
from ... base_types import AnimationNode, AutoSelectDataType
from ... sockets.info import toIdName

class objCPConvertNode(bpy.types.Node, AnimationNode):
    bl_idname = "an_objCPConvertNode"
    bl_label = "Read & Convert CPs"
    bl_width = 100

    dataType = StringProperty(default = "Generic", update = AnimationNode.refresh)
    lastCorrectionType = IntProperty()

    fixedOutputDataType = BoolProperty(name = "Fixed Data Type", default = False,
        description = "When activated the output type does not automatically change",
        update = AnimationNode.refresh)

    def create(self):
        self.newInput("Object", "Object", "inpObj")
        self.newInput("Text", "CP Name", "cpName")
        self.newOutput(self.dataType, "Output", "new")

        if not self.fixedOutputDataType:
            self.newSocketEffect(AutoSelectDataType(
                "dataType", [self.outputs[0]], ignore = {"Generic"}))

    def draw(self, layout):
        row = layout.row(align = True)
        self.invokeSelector(row, "DATA_TYPE", "assignOutputType", text = "to " + self.dataType)
        icon = "LOCKED" if self.fixedOutputDataType else "UNLOCKED"
        row.prop(self, "fixedOutputDataType", icon = icon, text = "")

        if self.lastCorrectionType == 2:
            layout.label("Conversion Failed", icon = "ERROR")

    def assignOutputType(self, dataType):
        self.fixedOutputDataType = True
        if self.dataType != dataType:
            self.dataType = dataType

    def execute(self, inpObj, cpName):
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj is not None and inpObj is None:
            cps = cpObj.keys()
            if cpName in cps:
                new, self.lastCorrectionType = self.outputs[0].correctValue(cpObj[cpName])
                return new
            else:
                return None
        elif inpObj is not None and cpName is not None:
            cps = inpObj.keys()
            if cpName in cps:
                new, self.lastCorrectionType = self.outputs[0].correctValue(inpObj[cpName])
                return new
            else:
                return None
        else:
            return None

It looks for a default object, if not found, or if the input object is entered, it uses the chosen object and checks that the Custom Property names exist in both cases. I think I have cracked it, thanks for all your help!

screen shot 2019-02-14 at 14 23 49

EDIT:

Some more images of the nodes in use:

screen shot 2019-02-14 at 15 30 10

screen shot 2019-02-14 at 15 27 58

mathlusiverse commented 5 years ago

@Clockmender Your nodes are looking really good.

The Blender property and collection are completely new to me, I was testing different ideas. So I put everything (creating objects, writing and reading custom properties) into one single Python file so I can edit it easily. I now have a decent version. The code is conceptually clean and easy to maintain. There are still some problems with it. Eventually I will split it into variable writer, and variable reader.

The main problem now is the way the user declare a variable. It is very clunky! I like the way Group Input Nodes define their name, and the way we pick a group when we want to invoke it later! It is much more safer and professional: user pick from a list. But I am not sure if I can implement this feature.

@OmarSquircleArt Right now I am editing each Python script using a plain notepad. When I done with editing. I run the setup script to copy all changes to Blender addon folder. Start the Blender with the switch -con to see the system console. Blender always start full screen, I have to resize it to about 75% of the screen size so I can see the system console. I then strat or load a previous animation node blender files. If there is an error, I have to close the Blender and edit the Python script and start all over again.

My question is:

  1. I resize the blender to a smaller size, save everything and close it. I restart blender next time. It is back to full size again! Is it possible to force blender not to start full size?
  2. If there is an error in the system console, I try to turn off the animation node in preference and refresh the addon folder then turn on the addon agin. But it doesn't work, is it possible?
  3. Any suggestion on streaming the workflow?
Clockmender commented 5 years ago

@mathlusiverse - I've got this node:

screen shot 2019-02-14 at 16 31 52

You can simply feed it through a Get List Element node, but I am working on being able to select from a list, I will keep you posted. I had hidden the Object Input on the image above as I was using default CP_Empty.

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

class getCPfromObj(bpy.types.Node, AnimationNode):
    bl_idname = "getCPfromObj"
    bl_label = "List CP Names"
    bl_width = 200

    mess = StringProperty()

    def create(self):
        self.newInput("Object", "Object", "inpObj")
        self.newOutput("Text List", "Custom Properties", "cusProp")

    def draw(self,layout):
        layout.label(self.mess,icon = 'INFO')

    def execute(self,inpObj):
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj is not None and inpObj is None:
            self.mess = 'CP_Empty Object'
            return cpObj.keys()
        elif inpObj is not None:
            self.mess = 'Chosen Object'
            return inpObj.keys()
        else:
            self.mess = 'No Objects'
            return None

EDIT:

Right now I am editing each Python script using a plain notepad

I got a ticking off for that from Jacques, so I now use Atom - Jacques said that plain text editors did not always use the same quote marks, etc. and that was giving me errors!

OmarEmaraDev commented 5 years ago

@mathlusiverse 1. Perhaps you should take a look at the -p directive in the documentation for CLI options.

    1. As far as I know, dynamic reloading doesn't work with AN because of Cython. So the only way to reload the addon is by closing and reopening Blender.

Maybe you should use VS Code with the Blender Development extension that Jacques created. I use Atom, but VS Code seems like a better option for you.

Clockmender commented 5 years ago

@mathlusiverse - This is the best I can do, one does not have access to bpy.data in the Class function, so you cannot build an enumerator list to choose from, unless someone knows how to do this...

screen shot 2019-02-15 at 19 57 35

Code for the CP selector node, feeding the "Read & Convert" node:

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

class getCPfromObj(bpy.types.Node, AnimationNode):
    bl_idname = "getCPfromObj"
    bl_label = "List CP Names"
    bl_width = 200

    indX = IntProperty(name = 'Index', min = 0, default = 0)

    def create(self):
        self.newInput("Object", "Object", "inpObj")
        self.newOutput("Text List", "Custom Properties", "cusProp")
        self.newOutput("Text", "Index Custom Property", "indxCP")

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

    def execute(self,inpObj):
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj is not None and inpObj is None:
            if self.indX >= len(cpObj.keys()):
                self.indX = len(cpObj.keys()) - 1
            self.label = 'Active CP: '+cpObj.keys()[self.indX]
            return cpObj.keys(), cpObj.keys()[self.indX]
        elif inpObj is not None:
            if self.indX >= len(inpObj.keys()):
                self.indX = len(inpObj.keys()) - 1
            self.label = 'Active CP: '+inpObj.keys()[self.indX]
            return inpObj.keys(), inpObj.keys()[self.indX]
        else:
            self.label = 'No Objects to Read'
            return None

It labels itself with the chosen Custom Property name.

mathlusiverse commented 5 years ago

@OmarSquircleArt Thanks for the suggestion. I will look into the Blender Development extension later.

@Clockmender Thanks for your help! The drop down list for variable name is definitely a 'high end' feature. I am cleaning up my code and will post mine later. Right now it is working, but there is still some problems; but the problems are not game-stopper. I am considering 3 Variable nodes now. The Variable Writer, Variable Reader, and Variable Manager. The manager node is an unnecessary evil. Currently useless variables are created as user type the variable names. I can see the cluttering of meaningless custom properties attached to the store (corresponds to cpObj in your code) and can delete them myself. But other user of the node should not be expected to manipulate the store themselves. Hence the Variable Manager is to allow end-user to add/delete variable names and change types in a safe manner.

I don't think the end-users should have any knowledge of the store (cpObj), hence I had avoided using any references of 'custom property' in the interface of the variable nodes. I call 'custom property name' the 'variable name' (please see my most recent screenshot). I hope to post something new soon.

EDIT: @Clockmender Can you zip your last node files and the Blender file in one single file and upload here. I have Blender 2.79 & 2.8 on my machine, I like to test your nodes because I think it is easier for me to follow your code and test different ideas if I can tinker the Blender file. Thanks.

Clockmender commented 5 years ago

@mathlusiverse:

The manager node is an unnecessary evil. Currently useless variables are created as user type the variable names.

This is because you are running AN in "Always Mode":

screen shot 2019-02-16 at 08 18 55

To stop this run in "Frame Changed" Mode, or some other than "Always", then just scrub the Timeline 1 frame, or click the Execute button in the AN Toolshelf.

screen shot 2019-02-16 at 08 19 10

The easiest way to clean up the cpObj is just delete it and Execute the node tree again, it only then creates the CP's to whatever "Writer" nodes you have in the node tree. Doing this can be put into a node, or I can delete the cpObj every execution, not sure about how effective this might be from a performance point of view, I will measure execute times if I do this. One other option is a boolean called "Cleanup" on the "Writer" node that deletes the cpObj if set to True, then sets it to False afterwards, all is then automatic and no need for a "Manager" node.

After deleting the cpObj - all is cleaned up and it is automatically re-created:

screen shot 2019-02-16 at 08 24 59

Files to follow, I will change the names from "Custom Property" to "Variable"

EDIT:

Files as promised, I am still looking at auto-deleting the cpObj, but have other things to do for Mrs. C first!

Archive.zip

This one deletes the cpObj, but of course it gets re-created next time you run the node tree and only the required variables get added back.

man_cps.py.zip

Doing it on the fly did not work, because at some tiny instance in time, there were not enough variables as described in the tree, so it failed, this method works. Add a load of Custom Properties to CpObj and then click the "Reset Variables" Button, all will be back to normal again.

I also altered the normal "List Variable" node so it doesn't show all of them, just the indexed one:

get_cps.py.zip

I think it is better like this, here is a screen shot, you just need to alter some nodes in my project:

screen shot 2019-02-16 at 10 52 01

Before Reset:

screen shot 2019-02-16 at 10 57 13

After Reset:

screen shot 2019-02-16 at 10 55 30

EDIT:

This means of course that if you run AN "Always" then click the reset button, all spurious variables get deleted!

And this is slightly better code for the function:

def resetNode(self):
        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj is not None:
            bpy.data.objects.remove(cpObj, True)
Clockmender commented 5 years ago

I have just discovered FloatVectorProperty types in Blender, so the new Variables Node looks like this:

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

class variableCPStore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableCPStore"
    bl_label = "Variable Store"
    bl_width_default = 200

    strV = StringProperty()
    booV = BoolProperty(default = True)
    mess = StringProperty()
    intV = IntProperty(default = 0)
    floV = FloatProperty(default = 0)
    vecV = FloatVectorProperty(subtype = 'XYZ')
    eulV = FloatVectorProperty(subtype = 'EULER')
    quaV = FloatVectorProperty(subtype = 'QUATERNION')
    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),
        ("BOOLEAN","Boolean","Boolean Rotation Variable","",6)]

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

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

    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")
        elif self.mode == "BOOLEAN":
            self.newInput("Boolean", "Input", "varInput")
            self.newOutput("Boolean", "Output", "varOutput")
        self.newInput("Boolean", "Process", "boolInput")
        self.newInput("Text", "Variable Name", "cpName")

    def execute(self,varInput,boolInput,cpName):
        if cpName == '':
            self.mess = 'Enter Variable Name'
            return None
        else:
            self.mess = ''
        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 == "BOOLEAN":
            if boolInput:
                self.booV = varInput
                varOutput = varInput
            else:
                varOutput = self.booV
        elif self.mode == "VECTOR":
            if boolInput:
                self.vecV = varInput
                varOutput = varInput
            else:
                varOutput = self.vecV
        elif self.mode == "EULER":
            if boolInput:
                self.eulV = varInput
                varOutput = varInput
            else:
                varOutput = self.eulV
        elif self.mode == "QUATERNION":
            if boolInput:
                self.quaV = varInput
                varOutput = varInput
            else:
                varOutput = self.quaV

        cpObj = bpy.data.objects.get('CP_Empty')
        if cpObj is None:
            bpy.ops.object.add(type='EMPTY',location=(0,0,0),radius = 0.3)
            bpy.context.active_object.name = 'CP_Empty'
            bpy.context.active_object.empty_draw_type = "SINGLE_ARROW"
            bpy.context.active_object.show_name = True
            bpy.context.active_object.layers[19] = True
            for i in range(18):
                bpy.context.active_object.layers[i] = False
            bpy.context.active_object.hide = True
            bpy.context.active_object.select = False
            cpObj = bpy.data.objects.get('CP_Empty')

        cpObj[cpName] = varOutput

        return varOutput
mathlusiverse commented 5 years ago

I was trying to implement better and safer way to add/delete variables. I have to give up for the moment. Right now I have two nodes: Variable Writer and Variable Reader.

They can be used to access the same variables at different place in the tree. The variable values are stored as custom properties of an empty object. The object is created automatically, and will be re-created whenever it is missing and a variable-write is triggered.

So far I am happy with the behavior of the writer and reader nodes, it works. However it should be more robust and friendly to create/delete variables. I am going to post my code here for the moment.

image

image

image

File #1 Shared by Writer and Reader

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

mlv_varTypeEnum = [
     # (identifier, displayName, description, icon, ordinal)
     # ordinal: unique constant
     ('STRING',     'String',     'String type variable',                    '',0),
     ('FLOAT',      'Float',      'Float type variable',                     '',1),
     ('INTEGER',    'Integer',    'Integer type variable',                   '',2),
     ('BOOLEAN',    'Boolean',    'Boolean type variable',                   '',3),
     ('VECTOR',     'Vector',     'Vector type variable',                    '',4),
     ('EULER',      'Euler',      'Euler (for rotation) type variable',      '',5),
     ('QUATERNION', 'Quaternion', 'Quaternion (for rotation) type variable', '',6) ]

def mlv_defaultValue(varType):
  if varType == 'STRING':
    return ''
  elif varType == 'INTEGER':
    return 0
  elif varType == 'FLOAT':
    return 0
  elif varType == 'BOOLEAN':
    return False
  elif varType == 'VECTOR':
    return Vector()
  elif varType == 'EULER':
    return Euler()
  elif varType == 'QUATERNION':
    return Quaternion()

def mlv_getStore():
  COLL_NAME = 'Animation Nodes Object Container' #  shared by all nodes
  STORE_NAME = 'AnimNodeVarStore' #  shared by all nodes
  store = bpy.data.objects.get(STORE_NAME)
  if store == None:
    store = bpy.data.objects.new(name = STORE_NAME, object_data = None) 
    try:  # find the existing collection
      coll = bpy.data.collections[COLL_NAME]
    except:
      pass
    if coll == None:  # not found
      # create new collection
      coll = bpy.data.collections.new(COLL_NAME)
      # link the newCol to the scene
      bpy.context.scene.collection.children.link(coll)
    bpy.data.collections[COLL_NAME].objects.link(store)
  return store

def mlv_getVarValue(store, varName, varType):
  try:
    val = store[varName]
  except:
    val = mlv_defaultValue(varType)
  if varType == 'STRING':
    return ''.join(chr(ascii) for ascii in val)
  else:
    return val

def mlv_setVarValue(store, varName, varType, newValue):
  if varType == 'STRING':
    chars = []
    for c in newValue:
      chars.append(ord(c))
    store[varName] = chars
  elif varType == 'FLOAT':
    store[varName] = newValue
  elif varType == 'INTEGER':
    store[varName] = round(newValue)
  elif varType == 'BOOLEAN':
    store[varName] = 1 if newValue else 0
  elif varType == 'VECTOR':
    store[varName] = [newValue.x, newValue.y, newValue.z]
  elif varType == 'EULER':
    store[varName] = [newValue.x, newValue.y, newValue.z]
  elif varType == 'QUATERNION':
    store[varName] = [newValue.x, newValue.y, newValue.z, newValue.w]

File #2 : Writer


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

class Mlv14VarWriter(bpy.types.Node, AnimationNode):
  bl_idname = 'Mlv14VarWriter'
  bl_label = 'Mlv14 Variable Writer'
  # write variable value to custom property of an object 
  #    shared by all variable writers and variable readers

  msg : StringProperty()

  varType : EnumProperty(name = 'Type', items = mlv_varTypeEnum, 
                        default = 'FLOAT',  update = AnimationNode.refresh)

  def draw(self,layout):
    layout.prop(self, 'varType')
    if self.msg != '':
      layout.label(text = self.msg, icon = 'ERROR')

  def create(self):
    self.val = None
    # newInOutput( Type, Name, Identifier )
    self.newInput('Text', 'Name', 'varName')
    if self.varType == 'STRING':
      self.newInput( 'Text', 'New Value', 'newValue')
    elif self.varType == 'INTEGER':
      self.newInput( 'Integer', 'New Value', 'newValue')
    elif self.varType == 'FLOAT':
      self.newInput( 'Float', 'New Value', 'newValue')
    elif self.varType == 'BOOLEAN':
      self.newInput( 'Boolean', 'New Value', 'newValue')
    elif self.varType == 'VECTOR':
      self.newInput( 'Vector', 'New Value', 'newValue')
    elif self.varType == 'EULER':
      self.newInput( 'Euler', 'New Value', 'newValue')
    elif self.varType == 'QUATERNION':
      self.newInput( 'Quaternion', 'New Value', 'newValue')
    self.newInput('Boolean', 'Set Value', 'setValue')

  def execute(self, varName, newValue, setValue):
    if varName == '':
      self.msg = 'Variable name missing'
      return 
    self.msg = ''
    if setValue:
      store = mlv_getStore()
      mlv_setVarValue(store, varName, self.varType, newValue)

File #3: Reader

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

class Mlv14VarReader(bpy.types.Node, AnimationNode):
  bl_idname = 'Mlv14VarReader'
  bl_label = 'Mlv14 Variable Reader'
  # read variable value from custom property of an object 
  #    shared by all variable writers and variable readers

  msg : StringProperty()

  varType : EnumProperty(name = 'Type', items = mlv_varTypeEnum, 
                        default = 'FLOAT',  update = AnimationNode.refresh)

  def draw(self,layout):
    layout.prop(self, 'varType')
    if self.msg != '':
      layout.label(text = self.msg, icon = 'ERROR')

  def create(self):
    self.val = None
    # newInOutput( Type, Name, Identifier )
    self.newInput('Text', 'Name', 'varName')
    if self.varType == 'STRING':
      self.newOutput('Text', 'Value', 'currValue')
    elif self.varType == 'INTEGER':
      self.newOutput('Integer', 'Value', 'currValue')
    elif self.varType == 'FLOAT':
      self.newOutput('Float', 'Value', 'currValue')
    elif self.varType == 'BOOLEAN':
      self.newOutput('Boolean', 'Value', 'currValue')
    elif self.varType == 'VECTOR':
      self.newOutput('Vector', 'Value', 'currValue')
    elif self.varType == 'EULER':
      self.newOutput('Euler', 'Value', 'currValue')
    elif self.varType == 'QUATERNION':
      self.newOutput('Quaternion', 'Value', 'currValue')

  def execute(self, varName):
    if varName == '':
      self.msg = 'Variable name missing'
      return mlv_defaultValue(self.varType)
    self.msg = ''
    store = mlv_getStore()
    return mlv_getVarValue(store, varName, self.varType)
mathlusiverse commented 5 years ago

@OmarSquircleArt I tested the variable writer and reader nodes, they both work as expected. I am happy the way it is. The only thing I like to improve is the process of create/delete/edit variable names/types.

The nodes are working properly already. It may be useful for others. Can you please comment on it. If you have suggestions and hints on how to improve the handling of variable creation/deletion, I will be happy to try it.

@Clockmender If you are considering working with Blender 2.8 (which I strongly encourage you to try), please do try my variable writer and reader nodes and comment on them! Thank you for your inspiration! :)

OmarEmaraDev commented 5 years ago

@mathlusiverse Alright. I will look into it tomorrow.

Clockmender commented 5 years ago

Hi @mathlusiverse my friend, I have 2.8 loaded, but not the latest and I don't have AN for it yet. I will be going that route soon, I need some spare time. Don't EVER retire from work, you are so chuffing busy all the time because your family think you have nothing to do.... grumble, grumble. Anyways I am pleased you have what you wanted and pleased to have helped along the way!

I am going to try your "No Data" object rather than an Empty in 2.79 in the next day, or so. I am just building a simple prototype Digital Audio Workstation (like Reason, or LMMS) in Blender at the moment to try to get this going as a project, if my efforts vaguely work....

Cheers, Clock.

mathlusiverse commented 5 years ago

@OmarSquircleArt @Clockmender Thanks for taking the time. I have uploaded a zip. It contains one Blender file and four python files (Periodic Trigger, Variable Writer, variable Reader and a variable manager) used for my most recent post (ver 14) .

Hopefully the whole thing does not depend on the particular folder structure of my local addon setup. If it works, it should open in Blender 2.8 and ready to test immediately. Just press SPACE to start the animation. Here is a screen shot of the Blender file upon opening.

image

Custom colors of the Variable writer/reader nodes do not stick. Don't know why.

For some reasons, sometimes the following message will show at the console when I quit the Blender, Need help hunting down the memory leak.

Error: Not freed memory blocks: 6, total unfreed memory 0.012619 MB

Attached file: mlv-14.zip

@Clockmender Indeed, I too have to slow down after I worked like crazy learning, developing and debugging the variable nodes.

Clockmender commented 5 years ago

I have altered my Store Node so the object is completely hidden:

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

class variableCPStore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableCPStore"
    bl_label = "Variable Store"
    bl_width_default = 200

    strV = StringProperty()
    booV = BoolProperty(default = True)
    mess = StringProperty()
    intV = IntProperty(default = 0)
    floV = FloatProperty(default = 0)
    vecV = FloatVectorProperty(subtype = 'XYZ')
    eulV = FloatVectorProperty(subtype = 'EULER')
    quaV = FloatVectorProperty(subtype = 'QUATERNION')
    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),
        ("BOOLEAN","Boolean","Boolean Rotation Variable","",6)]

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

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

    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")
        elif self.mode == "BOOLEAN":
            self.newInput("Boolean", "Input", "varInput")
            self.newOutput("Boolean", "Output", "varOutput")
        self.newInput("Boolean", "Process", "boolInput")
        self.newInput("Text", "Variable Name", "cpName")

    def execute(self,varInput,boolInput,cpName):
        if cpName == '':
            self.mess = 'Enter Variable Name'
            return None
        else:
            self.mess = ''
        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 == "BOOLEAN":
            if boolInput:
                self.booV = varInput
                varOutput = varInput
            else:
                varOutput = self.booV
        elif self.mode == "VECTOR":
            if boolInput:
                self.vecV = varInput
                varOutput = varInput
            else:
                varOutput = self.vecV
        elif self.mode == "EULER":
            if boolInput:
                self.eulV = varInput
                varOutput = varInput
            else:
                varOutput = self.eulV
        elif self.mode == "QUATERNION":
            if boolInput:
                self.quaV = varInput
                varOutput = varInput
            else:
                varOutput = self.quaV

        cpObj = bpy.data.objects.get('VAR_Store')
        if cpObj is None:
            bpy.data.objects.new('VAR_Store', None)
            cpObj = bpy.data.objects.get('VAR_Store')

        cpObj[cpName] = varOutput

        return varOutput

Cheers, Clock.

EDIT:

Look, no variable objects!

screen shot 2019-02-26 at 14 00 21

OmarEmaraDev commented 5 years ago

@mathlusiverse Here are some points after having an initial look at the nodes:

  1. I assume you are only interested in storing the variable in memory and not disk. In which case, you don't need an object to store your variables. Just store them anywhere in Animation Nodes. One possibility would be to define an empty dictionary outside of the class and add entries based on the execution code. Check any node that performs caching as an example.
  2. Do we really need an explicit type enum? Can't we just use automatic socket type conversion? What do you think?
  3. Don't use labels to output errors. Instead, use Error UI Extensions. Moreover, using EXCEPTION errorHandlingType will set defaults automatically, so you can remove the defaults code. To know more about this, just search for nodes that sets the errorHandlingType attribute.
  4. Generally, nodes should have at least one input and one output. So having a node that have no output is a bad idea.
  5. In your example, you have your setters and getters in different execution units. So there is no telling which will execute before which. Will we set then get or will we get and then set? This ambiguity is not tolerated in Animation Nodes. Jacques requires nodes to be "side effects free" and this node doesn't fit the criteria. This is the main reason why we don't have such nodes in Animation Nodes. One has to really think this through to make sure there are no side effects.
Clockmender commented 5 years ago
  1. I assume you are only interested in storing the variable in memory and not disk. In which case, you don't need an object to store your variables. Just store them anywhere in Animation Nodes. One possibility would be to define an empty dictionary outside of the class and add entries based on the execution code. Check any node that performs caching as an example.

@OmarSquircleArt is this possible with a multitude of variable types? We have text, float, vector, boolean, euler and quaternion variables in here. I am not sure I understand what you mean by "Just store them anywhere in Animation Nodes".

4. Generally, nodes should have at least one input and one output. So having a node that have no output is a bad idea.

@OmarSquircleArt , @mathlusiverse my "Store Variable" node does output the value, so is this OK?

5. In your example, you have your setters and getters in different execution units. So there is no telling which will execute before which. Will we set then get or will we get and then set?

@OmarSquircleArt could you please explain this for us, I am not sure what you are saying here, is there a way to execute some nodes before others? I think the preferred option would be "set then get" as an order.

Is the type of error handling you quoted in your point 3 available in Blender 2.79/AN 2.0?

Thanks, Clock.

Clockmender commented 5 years ago

@OmarSquircleArt I think I answered one of my questions:

screen shot 2019-02-26 at 15 38 37

screen shot 2019-02-26 at 15 44 25

However, looking at the execution times, our nodes are almost 10 times quicker...

Have I made a mistake somewhere? The more I run the animation, the worse the times get, so is there a way to clear everything out at frame 1 for example?

Cheers, Clock.

OmarEmaraDev commented 5 years ago

@Clockmender

  1. Yes, it works with any type, it can store any python object, which is about everything. By storing them in Animation Nodes I meant the AN module. So they will be stored, for instance, in AN.nodes.generic.variable_node.variables where variables is the global dictionary I mentioned. See this node as a reference.
  2. Yah, outputting the value is a good idea. I was talking about MLV node.
  3. Animation Nodes executes execution units in no defined order and we can't force her to follow a certain order. So we can't implement the "set then get" policy you mentioned. Sometimes AN will do it set then get and other times get then set. I hope you can already see why this is a problem.
  4. I don't think so, it is available in 2.1 only.
  5. I am not sure why it is getting slower. However, expression nodes do some extra things, so you can't really compare a native node to an expression. A good thing to do is enable Measure Execution Time and measure the minimum time of each node instead.
Clockmender commented 5 years ago

attribute_test.blend.zip

Here's the blend file, just run the animation several times over and the execution times just ramp up.

I will try with a home-made node...

OmarEmaraDev commented 5 years ago

I just tried it, it only takes 0.08ms on my machine. So not sure why this is happening in your case.

Clockmender commented 5 years ago

Leave the animation running for 3 minutes.....

Clockmender commented 5 years ago

That's weird, I just restarted Blender and it no longer ramps up, oh well....

Thanks!

Clockmender commented 5 years ago

@OmarSquircleArt - So this works:

setattr(animation_nodes, "float_test", x*y) if y > 1 else setattr(animation_nodes, "float_test", 0)

(y is the Frame Value... x is a float)

and this is useful for us:

screen shot 2019-02-26 at 16 33 40

I shall look at using this instead of the object over the next few days. Thanks again for the insight and please don't despair too much at my lack of knowledge!

Clockmender commented 5 years ago

I don't know if I am really missing something @OmarSquircleArt but this works in an expression node:

screen shot 2019-02-26 at 18 06 37

But this code does not work, I can find no idea of how to do this in a node anywhere:

def execute(self,varInput,boolInput):
        if '.' in self.name:
            varName = 'VAR_'+self.name.split('.')[1]
        else:
            varName = 'VAR_000'
        setattr(animation_nodes, varName, varInput)
        return varInput

What am I missing please?

OmarEmaraDev commented 5 years ago

@Clockmender Are you getting errors? Your indentations seems wrong. Is animation_nodes defined somewhere?

Clockmender commented 5 years ago

@OmarSquircleArt Just the red border, no useful errors. It’s just a code snippet, indentation is correct in my node file. If I comment out the setattr line the node works. I don’t understand why the code works in an Expression Node but not in a custom node.

OmarEmaraDev commented 5 years ago

@Clockmender Is animation_nodes defined somewhere? The expression node have animation_nodes defined, so you can use it. So make sure you have animation_nodes defined. Or simply use the node itself to store the variables as I described before.

mathlusiverse commented 5 years ago

@OmarSquircleArt

Thanks for the comment, here are some of my immediate thoughts.

  1. I assume you are only interested in storing the variable in memory and not disk. ... Yes, the variable is meant to be used transient: good only during the animation, no need to save across session.

  2. Do we really need an explicit type enum? Can't we just use automatic socket type conversion? ... Definitely don't need need another explicit type enum. Would love to use existing enum. Need to learn more abut it first. Automatic socket type conversion is a good idea. Need to learn more about it too.

  3. Don't use labels to output errors. Instead, use Error UI Extensions. ... OK

  4. Generally, nodes should have at least one input and one output. ... I am trying to emphasis the two different roles of variable writers and readers. As in some other drag-and-drop visual scripting languages, they have sink nodes (no output sockets) and source nodes (no input sockets). My original Variable nodes is writer/reader combined, It has both the input (write) and output (read). If necessary, we can just re-combine the writer and reader back to the same node. Also see my thoughts on 5.

  5. In your example, you have your setters and getters in different execution units. So there is no telling which will execute before which. ... This seems to be the real challenge. If AN has no built-in capability to allow custom ordering of node execution, then we will have a race between writing and reading variables! It would be nice if AN supports something like pre-update, update, post-update in each frame update (the main 'game' loop). Thus nodes designated pre-update will be run first. Without such mechanism, the write/read race may cause the 'get' off by at most one frame. The only way to make sure 'set' before 'get' is to have ONE single variable node with both write and read resides in the same node.

In my original scenario, I have a computational-intensive vector variable that changes slowly. So I want to re-calculate it every 15 frames. In sch scenario, my variable writer nodes and variable reader nodes do the job. The reason is because the variable changes slowly, and a one frame off due to write/read race do not have noticeable difference in animation. This of course may not be the case in other use case.

Clockmender commented 5 years ago

@OmarSquircleArt OK I got there in the end:

screen shot 2019-02-26 at 21 22 51

I use the .name of the node to set the key name for the dictionary entry as this is unique to the project and requires no user input.

Here is the node code:

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

varStore = {}

class variableATTRtore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableATTRStore"
    bl_label = "Variable ATTR"
    bl_width_default = 200

    mess = StringProperty()
    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),
        ("BOOLEAN","Boolean","Boolean Rotation Variable","",6)]

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

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

    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")
        elif self.mode == "BOOLEAN":
            self.newInput("Boolean", "Input", "varInput")
            self.newOutput("Boolean", "Output", "varOutput")
        self.newInput("Boolean", "Update Variable", "boolInput")

    def execute(self,varInput,boolInput):
        if '.' in self.name:
            key = 'VAR_'+self.name.split('.')[1]
        else:
            key = 'VAR_000'
        if boolInput:
            varStore[key] = varInput
        self.label = 'Var Key: '+key
        return varStore.get(key)

I think that Automatic Socket Type conversion is a 2.1 thing? So I use the enum for now...

Thanks for your help, I really appreciate it, even if I don't understand everything at once!

OmarEmaraDev commented 5 years ago

@mathlusiverse @Clockmender

Automatic type selection:

Import DataTypeSelectorSocket from base_types:

from ... base_types import AnimationNode, DataTypeSelectorSocket

Create a property to hold the type:

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

Create an automatic selection input:

self.newInput(DataTypeSelectorSocket("Input Name", "inputIdentifier", "assignedType"))

For Clock

You should define enum items outside the node class.

For MLV

Ok, see if a single node would be more appropriate. Note, however, that this is not the only way to define execution order, the standard way is to make sure the the latter execution unit depends on the former execution unit. The dependency needn't be effective in any way, for instance, consider the following two node tree:

Node Tree

In this node tree, we have two execution units, each have only one node, they execute in no defined order. So they will execute in arbitrary order. Now, lets look at this node tree:

Node Tree

We setup the node tree such that is is composed of a single execution unit, we made it such that the second node depends on the first, so Animation Nodes must execute the first node first. Notice that the latter don't really depend on the former, the latter simply ignores whatever the former gave which is None anyway. This is generally how you define order in Animation Nodes. That's why I said that nodes should generally have at least one input and one output.

Clockmender commented 5 years ago

@OmarSquircleArt Thank you, I understand now, but the DatatypeSelectorSocket is 2.1, I think, this does not work in 2.79/AN 2.0 - I will change the node when I upgrade to 2.8/AN 2.1.

We were trying to get rid of lots of connectors across the node tree, but I did not realise this screws the execute order, so I will just use the one node from now on and connect it as required.

I have to say that the manual for node development is a little light in information of this depth, maybe we can improve the docs at some point.

Anyway thanks again for all your input, it has been most rewarding for me and I am sure also for @mathlusiverse

Cheers, Clock.

EDIT:

Revised node setup:

screen shot 2019-02-27 at 09 39 34

Revised node code, for 2.79/AN 2.0:

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

varStore = {}
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),
    ("BOOLEAN","Boolean","Boolean Rotation Variable","",6)]

class variableATTRtore(bpy.types.Node, AnimationNode):
    bl_idname = "an_variableATTRStore"
    bl_label = "Store Variable"
    bl_width_default = 200

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

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

    def drawAdvanced(self, layout):
        self.invokeFunction(layout, "clearCache", text = "Clear Variables")

    def clearCache(self):
        varStore.clear()

    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")
        elif self.mode == "BOOLEAN":
            self.newInput("Boolean", "Input", "varInput")
            self.newOutput("Boolean", "Output", "varOutput")
        self.newInput("Boolean", "Update Variable", "boolInput")

    def execute(self,varInput,boolInput):
        if '.' in self.name:
            key = 'VAR_'+self.name.split('.')[1]
        else:
            key = 'VAR_000'
        self.label = 'Var Key: '+key
        if boolInput:
            varStore[key] = varInput

        return varStore.get(key)
OmarEmaraDev commented 5 years ago

@Clockmender You can use 2.1 with 2.79. Just compile the v2.1 branch in this case. As for the development guide, yes, I agree. However, I am not sure if it is worth writing/improving; there isn't a lot of people developing Animation Nodes, so I think our efforts are better directed somewhere else.

Clockmender commented 5 years ago

@OmarSquircleArt

I am not sure if it is worth writing/improving; there isn't a lot of people developing Animation Nodes, so I think our efforts are better directed somewhere else.

I agree, let me know if I can help at all with AN development, etc.