From 4c7faca61bfd6f2acd262157a3f12a989e11d12f Mon Sep 17 00:00:00 2001 From: Jannik Vogel Date: Thu, 13 Feb 2025 23:04:06 +0100 Subject: [PATCH] Add blender simulation --- simulate.py | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100755 simulate.py diff --git a/simulate.py b/simulate.py new file mode 100755 index 0000000..82d6e26 --- /dev/null +++ b/simulate.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python3 + +# Ugly, but working: +# +# 1. Start blender +# 2. Import your light strip with one vertex per LED, ensure vertex index matches LED index +# 3. Load this script in blender and run it +# 4. Start animation +# 5. Switch to material preview or rendered view +# 6. Broadcast your WLED DNRGB updates to the config below + +#FIXME: This might need a bit of https://b3d.interplanety.org/en/accessing-custom-attributes-created-in-geometry-nodes/ to get the input indices + +HOST = "localhost" +PORT = 21324 + +#import LeitLightLightLeitLeidLite as l6 + +import bpy +import mathutils + +import socket + + + +obj = bpy.data.objects['strips'] + +if True: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind((HOST, PORT)) + sock.setblocking(False) + +ledCount = len(obj.data.vertices) + +print("%d LEDs" % ledCount) +state = bytearray([0x00,0x00,0x00] * ledCount) +durations = bytearray([0x00] * ledCount) + + +mesh = obj.data #bpy.context.collection.objects["strips"].data +def setAttrib(attribName, data): + if attribName in mesh.attributes: + attribute = mesh.attributes[attribName] + else: + attribute = mesh.attributes.new(name=attribName, type="FLOAT", domain="POINT") + #attribute_values = [i for i in range(len(mesh.vertices))] + attribute.data.foreach_set("value", data) + +# Created using https://extensions.blender.org/add-ons/node-to-python/ +def createMaterial(materialName): + + mat = bpy.data.materials.new(name = materialName) + mat.use_nodes = True + + material = mat.node_tree + #start with a clean node tree + for node in material.nodes: + material.nodes.remove(node) + material.color_tag = 'NONE' + material.description = "" + material.default_group_node_width = 140 + + #node Material Output + material_output = material.nodes.new("ShaderNodeOutputMaterial") + material_output.name = "Material Output" + material_output.is_active_output = True + material_output.target = 'ALL' + #Displacement + material_output.inputs[2].default_value = (0.0, 0.0, 0.0) + #Thickness + material_output.inputs[3].default_value = 0.0 + + #node Emission + emission = material.nodes.new("ShaderNodeEmission") + emission.name = "Emission" + #Color + emission.inputs[0].default_value = (1.0, 0.0, 0.018193421885371208, 1.0) + + #node Add Shader + add_shader = material.nodes.new("ShaderNodeAddShader") + add_shader.name = "Add Shader" + + #node Emission.001 + emission_001 = material.nodes.new("ShaderNodeEmission") + emission_001.name = "Emission.001" + #Color + emission_001.inputs[0].default_value = (0.021969836205244064, 1.0, 0.0, 1.0) + + #node Add Shader.001 + add_shader_001 = material.nodes.new("ShaderNodeAddShader") + add_shader_001.name = "Add Shader.001" + + #node Emission.002 + emission_002 = material.nodes.new("ShaderNodeEmission") + emission_002.name = "Emission.002" + #Color + emission_002.inputs[0].default_value = (0.0, 0.0016676115337759256, 1.0, 1.0) + + #node Attribute + attribute = material.nodes.new("ShaderNodeAttribute") + attribute.name = "Attribute" + attribute.attribute_name = "red" + attribute.attribute_type = 'GEOMETRY' + + #node Attribute.001 + attribute_001 = material.nodes.new("ShaderNodeAttribute") + attribute_001.name = "Attribute.001" + attribute_001.attribute_name = "green" + attribute_001.attribute_type = 'GEOMETRY' + + #node Attribute.002 + attribute_002 = material.nodes.new("ShaderNodeAttribute") + attribute_002.name = "Attribute.002" + attribute_002.attribute_name = "blue" + attribute_002.attribute_type = 'GEOMETRY' + + + #Set locations + material_output.location = (300.0, 300.0) + emission.location = (-90.9997329711914, 320.24908447265625) + add_shader.location = (119.9999771118164, 290.62481689453125) + emission_001.location = (-95.54425048828125, 215.4246063232422) + add_shader_001.location = (115.45545959472656, 163.01229858398438) + emission_002.location = (-100.0887680053711, 87.81208801269531) + attribute.location = (-358.0585021972656, 399.44940185546875) + attribute_001.location = (-350.34014892578125, 246.54885864257812) + attribute_002.location = (-354.8846740722656, 114.37872314453125) + + #Set dimensions + material_output.width, material_output.height = 140.0, 100.0 + emission.width, emission.height = 140.0, 100.0 + add_shader.width, add_shader.height = 140.0, 100.0 + emission_001.width, emission_001.height = 140.0, 100.0 + add_shader_001.width, add_shader_001.height = 140.0, 100.0 + emission_002.width, emission_002.height = 140.0, 100.0 + attribute.width, attribute.height = 140.0, 100.0 + attribute_001.width, attribute_001.height = 140.0, 100.0 + attribute_002.width, attribute_002.height = 140.0, 100.0 + + #emission.Emission -> add_shader.Shader + material.links.new(emission.outputs[0], add_shader.inputs[0]) + #emission_001.Emission -> add_shader.Shader + material.links.new(emission_001.outputs[0], add_shader.inputs[1]) + #emission_002.Emission -> add_shader_001.Shader + material.links.new(emission_002.outputs[0], add_shader_001.inputs[1]) + #add_shader.Shader -> add_shader_001.Shader + material.links.new(add_shader.outputs[0], add_shader_001.inputs[0]) + #attribute.Fac -> emission.Strength + material.links.new(attribute.outputs[2], emission.inputs[1]) + #attribute_002.Fac -> emission_002.Strength + material.links.new(attribute_002.outputs[2], emission_002.inputs[1]) + #attribute_001.Fac -> emission_001.Strength + material.links.new(attribute_001.outputs[2], emission_001.inputs[1]) + #add_shader_001.Shader -> material_output.Surface + material.links.new(add_shader_001.outputs[0], material_output.inputs[0]) + return mat + + +# Created using https://extensions.blender.org/add-ons/node-to-python/ +def createGeoNodes(geoNodeName, material): + geometry_nodes = bpy.data.node_groups.new(type = 'GeometryNodeTree', name = geoNodeName) + + geometry_nodes.color_tag = 'NONE' + geometry_nodes.description = "" + geometry_nodes.default_group_node_width = 140 + + + geometry_nodes.is_modifier = True + + #geometry_nodes interface + #Socket Geometry + geometry_socket = geometry_nodes.interface.new_socket(name = "Geometry", in_out='OUTPUT', socket_type = 'NodeSocketGeometry') + geometry_socket.attribute_domain = 'POINT' + + #Socket Geometry + geometry_socket_1 = geometry_nodes.interface.new_socket(name = "Geometry", in_out='INPUT', socket_type = 'NodeSocketGeometry') + geometry_socket_1.attribute_domain = 'POINT' + + + #initialize geometry_nodes nodes + #node Group Input + group_input = geometry_nodes.nodes.new("NodeGroupInput") + group_input.name = "Group Input" + + #node Group Output + group_output = geometry_nodes.nodes.new("NodeGroupOutput") + group_output.name = "Group Output" + group_output.is_active_output = True + + #node Instance on Points + instance_on_points = geometry_nodes.nodes.new("GeometryNodeInstanceOnPoints") + instance_on_points.name = "Instance on Points" + #Selection + instance_on_points.inputs[1].default_value = True + #Pick Instance + instance_on_points.inputs[3].default_value = False + #Instance Index + instance_on_points.inputs[4].default_value = 0 + #Rotation + instance_on_points.inputs[5].default_value = (0.0, 0.0, 0.0) + #Scale + instance_on_points.inputs[6].default_value = (1.0, 1.0, 1.0) + + #node Ico Sphere + ico_sphere = geometry_nodes.nodes.new("GeometryNodeMeshIcoSphere") + ico_sphere.name = "Ico Sphere" + #Radius + ico_sphere.inputs[0].default_value = 1.0 + #Subdivisions + ico_sphere.inputs[1].default_value = 1 + + #node Store Named Attribute + store_named_attribute = geometry_nodes.nodes.new("GeometryNodeStoreNamedAttribute") + store_named_attribute.name = "Store Named Attribute" + store_named_attribute.data_type = 'INT' + store_named_attribute.domain = 'POINT' + #Selection + store_named_attribute.inputs[1].default_value = True + #Name + store_named_attribute.inputs[2].default_value = "index" + + #node Index + index = geometry_nodes.nodes.new("GeometryNodeInputIndex") + index.name = "Index" + + #node Realize Instances + realize_instances = geometry_nodes.nodes.new("GeometryNodeRealizeInstances") + realize_instances.name = "Realize Instances" + #Selection + realize_instances.inputs[1].default_value = True + #Realize All + realize_instances.inputs[2].default_value = True + #Depth + realize_instances.inputs[3].default_value = 0 + + #node Capture Attribute + capture_attribute = geometry_nodes.nodes.new("GeometryNodeCaptureAttribute") + capture_attribute.name = "Capture Attribute" + capture_attribute.active_index = 0 + capture_attribute.capture_items.clear() + capture_attribute.capture_items.new('FLOAT', "Index") + capture_attribute.capture_items["Index"].data_type = 'INT' + capture_attribute.domain = 'POINT' + + #node Set Material + set_material = geometry_nodes.nodes.new("GeometryNodeSetMaterial") + set_material.name = "Set Material" + #Selection + set_material.inputs[1].default_value = True + set_material.inputs[2].default_value = material + + + + + + #Set locations + group_input.location = (-537.77783203125, 192.87576293945312) + group_output.location = (1020.145263671875, 338.443115234375) + instance_on_points.location = (-40.82560348510742, 80.0461654663086) + ico_sphere.location = (-380.4179382324219, -85.53570556640625) + store_named_attribute.location = (503.62579345703125, 345.2210388183594) + index.location = (-583.5319213867188, 81.38147735595703) + realize_instances.location = (235.0821075439453, 147.38453674316406) + capture_attribute.location = (-276.59991455078125, 312.9664306640625) + set_material.location = (815.2620239257812, 334.15179443359375) + + #Set dimensions + group_input.width, group_input.height = 140.0, 100.0 + group_output.width, group_output.height = 140.0, 100.0 + instance_on_points.width, instance_on_points.height = 140.0, 100.0 + ico_sphere.width, ico_sphere.height = 140.0, 100.0 + store_named_attribute.width, store_named_attribute.height = 140.0, 100.0 + index.width, index.height = 140.0, 100.0 + realize_instances.width, realize_instances.height = 140.0, 100.0 + capture_attribute.width, capture_attribute.height = 140.0, 100.0 + set_material.width, set_material.height = 140.0, 100.0 + + #initialize geometry_nodes links + #ico_sphere.Mesh -> instance_on_points.Instance + geometry_nodes.links.new(ico_sphere.outputs[0], instance_on_points.inputs[2]) + #group_input.Geometry -> capture_attribute.Geometry + geometry_nodes.links.new(group_input.outputs[0], capture_attribute.inputs[0]) + #capture_attribute.Geometry -> instance_on_points.Points + geometry_nodes.links.new(capture_attribute.outputs[0], instance_on_points.inputs[0]) + #index.Index -> capture_attribute.Index + geometry_nodes.links.new(index.outputs[0], capture_attribute.inputs[1]) + #capture_attribute.Index -> store_named_attribute.Value + geometry_nodes.links.new(capture_attribute.outputs[1], store_named_attribute.inputs[3]) + #instance_on_points.Instances -> realize_instances.Geometry + geometry_nodes.links.new(instance_on_points.outputs[0], realize_instances.inputs[0]) + #realize_instances.Geometry -> store_named_attribute.Geometry + geometry_nodes.links.new(realize_instances.outputs[0], store_named_attribute.inputs[0]) + #store_named_attribute.Geometry -> set_material.Geometry + geometry_nodes.links.new(store_named_attribute.outputs[0], set_material.inputs[0]) + #set_material.Geometry -> group_output.Geometry + geometry_nodes.links.new(set_material.outputs[0], group_output.inputs[0]) + return geometry_nodes + + + + + +def my_handler(scene): + global state + print("Frame Change", scene.frame_current) + + + try: + data, addr = sock.recvfrom(4096) + #print("received message: %s" % data) + + assert(data[0] == 4) # DNRGB + duration = data[1] + baseIndex = (data[2] << 8) | data[3] + payloadLength = len(data) - 4 + assert(payloadLength % 3 == 0) + count = payloadLength // 3 + newState = data[4:] + + + beforeCount = baseIndex + afterIndex = baseIndex + count + state = state[0:beforeCount*3] + newState + state[afterIndex*3:] + + except BlockingIOError: + pass + + + setAttrib('red', [x / 255.0 for x in state[0::3]]) + setAttrib('green', [x / 255.0 for x in state[1::3]]) + setAttrib('blue', [x / 255.0 for x in state[2::3]]) + +def regHandler(my_handler): + for func in bpy.app.handlers.frame_change_post[:]: + if func.__name__ == my_handler.__name__: + bpy.app.handlers.frame_change_post.remove(func) + bpy.app.handlers.frame_change_pre.append(my_handler) + +material = createMaterial("l6-geometry-nodes") +geometry_nodes = createGeoNodes("l6-material", material) + +mod = obj.modifiers.new('l6-modifier', 'NODES') +mod.node_group = geometry_nodes + +regHandler(my_handler) +