Closed mathlusiverse closed 5 years ago
This works:
Maybe I can knock up a quick pair of nodes to do this easily.
This works:
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 :-)
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.
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.
@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:
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.
@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.
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.
@Clockmender Hi, I am not entirely sure what you want to do, but:
getExecutionCode
method should take two parameters, self
and required
. You only provided self
.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.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])"
@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:
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.
@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.
@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:
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....
@Clockmender The problem is in this line:
self.newInput("Object", "Object", "Object", "obj")
It should be:
self.newInput("Object", "Object", "obj")
@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...
@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:
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.
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.
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 :-)
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
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.
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:
@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!
EDIT:
Some more images of the nodes in use:
@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:
@mathlusiverse - I've got this node:
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!
@mathlusiverse 1. Perhaps you should take a look at the -p
directive in the documentation for CLI options.
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.
@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...
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.
@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.
@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":
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.
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:
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!
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.
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:
I think it is better like this, here is a screen shot, you just need to alter some nodes in my project:
Before Reset:
After Reset:
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)
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
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.
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)
@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! :)
@mathlusiverse Alright. I will look into it tomorrow.
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.
@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.
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.
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!
@mathlusiverse Here are some points after having an initial look at the nodes:
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.
- 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.
@OmarSquircleArt I think I answered one of my questions:
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.
@Clockmender
AN.nodes.generic.variable_node.variables
where variables is the global dictionary I mentioned. See this node as a reference.set then get
and other times get then set
. I hope you can already see why this is a problem.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...
I just tried it, it only takes 0.08ms on my machine. So not sure why this is happening in your case.
Leave the animation running for 3 minutes.....
That's weird, I just restarted Blender and it no longer ramps up, oh well....
Thanks!
@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:
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!
I don't know if I am really missing something @OmarSquircleArt but this works in an expression node:
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?
@Clockmender Are you getting errors? Your indentations seems wrong. Is animation_nodes
defined somewhere?
@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.
@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.
@OmarSquircleArt
Thanks for the comment, here are some of my immediate thoughts.
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.
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.
Don't use labels to output errors. Instead, use Error UI Extensions. ... OK
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.
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.
@OmarSquircleArt OK I got there in the end:
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!
@mathlusiverse @Clockmender
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"))
You should define enum items outside the node class.
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:
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:
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.
@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:
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)
@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.
@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.
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.