From 3850ec011ca11610a2efaa3de138af15cf32cb7f Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Mon, 14 Nov 2022 22:32:32 -0500 Subject: [PATCH] Support generators and add City Builder example --- README.md | 32 ++++++++++++++++++++++++++++++++ __init__.py | 2 +- api/node_mapper.py | 2 ++ api/tree.py | 9 ++++++++- api/types.py | 27 ++++++++++++++++----------- examples/City Builder.py | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 13 deletions(-) create mode 100644 examples/City Builder.py diff --git a/README.md b/README.md index 00ac33f..885f8e7 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,38 @@ socket_integer * 2 # creates a `Math` node that multiplies the `Integer` node by socket_integer = random_value(data_type='INT', seed=socket_integer) # use the `Integer` node as the seed for a new random integer ``` +### Generators +Tree builder functions can also be generators: + +```python +@tree +def cube_and_cylinder(): + yield cube() + yield cylinder().mesh +``` + +Because all of the generated values are `Geometry` types, they will be automatically joined with a *Join Geometry* node and sent as the output. + +You can `yield` other types as well, however: + +```python +@tree +def cube_cylinder_and_int(): + yield cube() # The first output of a Geometry Nodes tree must be `Geometry` + yield cylinder() + yield 5 +``` + +This will create three outputs, one for the cube, one for the cylinder, and one for an *Integer* node with the value `5`. + +### Default Values +```python +@tree +def cube_with_height(height: Float = 0.5): + return cube(size=combine_xyz(z=height)) +``` +The value `0.5` will show as the default in the Geometry Nodes modifier. + ### Available Nodes Every node available in your current version of Blender can be used as a function. The name will be converted to snake case: ```python diff --git a/__init__.py b/__init__.py index 912bb81..d7d2ad3 100644 --- a/__init__.py +++ b/__init__.py @@ -102,7 +102,7 @@ def register(): bpy.types.Scene.geometry_script_settings = bpy.props.PointerProperty(type=GeometryScriptSettings) - bpy.app.timers.register(auto_resolve) + bpy.app.timers.register(auto_resolve, persistent=True) def unregister(): bpy.utils.unregister_class(TEXT_MT_templates_geometryscript) diff --git a/api/node_mapper.py b/api/node_mapper.py index 8950df2..de81182 100644 --- a/api/node_mapper.py +++ b/api/node_mapper.py @@ -34,6 +34,8 @@ def build_node(node_type): State.current_node_tree.links.new(constant._socket, node_input) outputs = {} for node_output in node.outputs: + if not node_output.enabled: + continue outputs[node_output.name.lower().replace(' ', '_')] = Type(node_output) if len(outputs) == 1: return list(outputs.values())[0] diff --git a/api/tree.py b/api/tree.py index 7d5fea5..974f245 100644 --- a/api/tree.py +++ b/api/tree.py @@ -66,7 +66,14 @@ def tree(name): # Run the builder function State.current_node_tree = node_group - outputs = builder(*builder_inputs) + if inspect.isgeneratorfunction(builder): + generated_outputs = [*builder(*builder_inputs)] + if all(map(lambda x: issubclass(type(x), Type) and x._socket.type == 'GEOMETRY', generated_outputs)): + outputs = join_geometry(geometry=generated_outputs) + else: + outputs = generated_outputs + else: + outputs = builder(*builder_inputs) # Create the output sockets for i, result in enumerate(_as_iterable(outputs)): diff --git a/api/types.py b/api/types.py index 756c472..f3c37d4 100644 --- a/api/types.py +++ b/api/types.py @@ -81,23 +81,28 @@ class Type: def __ge__(self, other): return self._compare(other, 'GREATER_EQUAL') + + def _get_xyz_component(self, component): + if self._socket.type != 'VECTOR': + raise Exception("`x`, `y`, `z` properties are not available on non-Vector types.") + separate_node = State.current_node_tree.nodes.new('ShaderNodeSeparateXYZ') + State.current_node_tree.links.new(self._socket, separate_node.inputs[0]) + return Type(separate_node.outputs[component]) + @property + def x(self): + return self._get_xyz_component(0) + @property + def y(self): + return self._get_xyz_component(1) + @property + def z(self): + return self._get_xyz_component(2) for standard_socket in list(filter(lambda x: 'NodeSocket' in x, dir(bpy.types))): name = standard_socket.replace('NodeSocket', '') if len(name) < 1: continue globals()[name] = type(name, (Type,), { 'socket_type': standard_socket, '__module__': Type.__module__ }) - if name == 'Vector': - def get_component(component): - @property - def get(self): - separate_node = State.current_node_tree.nodes.new('ShaderNodeSeparateXYZ') - State.current_node_tree.links.new(self._socket, separate_node.inputs[0]) - return Type(separate_node.outputs[component]) - return get - globals()[name].x = get_component(0) - globals()[name].y = get_component(1) - globals()[name].z = get_component(2) if name == 'Int': class IntIterator: def __init__(self, integer): diff --git a/examples/City Builder.py b/examples/City Builder.py new file mode 100644 index 0000000..7a95f81 --- /dev/null +++ b/examples/City Builder.py @@ -0,0 +1,32 @@ +# Create a curve object as the base. +# In Edit Mode, use the Draw tool to create new roads in the city. + +from geometry_script import * + +@tree("City Builder") +def city_builder(geometry: Geometry, building_size_min: Vector = (0.1, 0.1, 0.2), building_size_max: Vector = (0.3, 0.3, 1), size_x: Float = 5.0, size_y: Float = 5.0, road_width: Float = 0.25, seed: Int = 0, resolution: Int = 60): + # Road geometry from input curves + road_points = geometry.curve_to_points().points + yield geometry.curve_to_mesh( + profile_curve=curve_line( + start=combine_xyz(x=road_width * -0.5), + end=combine_xyz(x=road_width / 2) + ) + ) + + # Randomly distribute buildings on a grid + building_points = grid( + size_x=size_x, size_y=size_y, + vertices_x=resolution, vertices_y=resolution + ).distribute_points_on_faces( + seed=seed + # Delete invalid building points based on proximity to a road + ).points.delete_geometry( + domain='POINT', + selection=road_points.geometry_proximity(target_element='POINTS', source_position=position()).distance < road_width * 2 + ) + random_scale = random_value(data_type='FLOAT_VECTOR', min=building_size_min, max=building_size_max, seed=seed + id()) + yield building_points.instance_on_points( + instance=cube(size=(1, 1, 1)).transform(translation=(0, 0, 0.5)), + scale=random_scale + ) \ No newline at end of file