kopia lustrzana https://github.com/carson-katri/geometry-script
Porównaj commity
3 Commity
1ffed55184
...
e3befbefc9
Autor | SHA1 | Data |
---|---|---|
Alfredo | e3befbefc9 | |
Carson Katri | 63a7516905 | |
Carson Katri | 467ae71ec6 |
|
@ -6,6 +6,7 @@ import os
|
|||
from .state import State
|
||||
from .types import *
|
||||
from .static.input_group import InputGroup
|
||||
from .static.curve import Curve
|
||||
from ..absolute_path import absolute_path
|
||||
|
||||
class OutputsList(dict):
|
||||
|
@ -13,6 +14,21 @@ class OutputsList(dict):
|
|||
__setattr__ = dict.__setitem__
|
||||
__delattr__ = dict.__delitem__
|
||||
|
||||
def set_or_create_link(x, node_input):
|
||||
if issubclass(type(x), Type):
|
||||
State.current_node_tree.links.new(x._socket, node_input)
|
||||
else:
|
||||
def link_constant():
|
||||
constant = Type(value=x)
|
||||
State.current_node_tree.links.new(constant._socket, node_input)
|
||||
if node_input.hide_value:
|
||||
link_constant()
|
||||
else:
|
||||
try:
|
||||
node_input.default_value = x
|
||||
except:
|
||||
link_constant()
|
||||
|
||||
def build_node(node_type):
|
||||
def build(_primary_arg=None, **kwargs):
|
||||
for k, v in kwargs.copy().items():
|
||||
|
@ -28,6 +44,13 @@ def build_node(node_type):
|
|||
argname = prop.identifier.lower().replace(' ', '_')
|
||||
if argname in kwargs:
|
||||
value = kwargs[argname]
|
||||
if isinstance(value, list) and len(value) > 0 and isinstance(value[0], Curve):
|
||||
for i, curve in enumerate(value):
|
||||
curve.apply(getattr(node, prop.identifier).curves[i])
|
||||
continue
|
||||
if isinstance(value, Curve):
|
||||
value.apply(getattr(node, prop.identifier).curves[0])
|
||||
continue
|
||||
if isinstance(value, enum.Enum):
|
||||
value = value.value
|
||||
setattr(node, prop.identifier, value)
|
||||
|
@ -40,20 +63,6 @@ def build_node(node_type):
|
|||
if node_input2.name.lower().replace(' ', '_') == argname and node_input2.type == node_input.type:
|
||||
all_with_name.append(node_input2)
|
||||
if argname in kwargs:
|
||||
def set_or_create_link(x, node_input):
|
||||
if issubclass(type(x), Type):
|
||||
State.current_node_tree.links.new(x._socket, node_input)
|
||||
else:
|
||||
def link_constant():
|
||||
constant = Type(value=x)
|
||||
State.current_node_tree.links.new(constant._socket, node_input)
|
||||
if node_input.hide_value:
|
||||
link_constant()
|
||||
else:
|
||||
try:
|
||||
node_input.default_value = x
|
||||
except:
|
||||
link_constant()
|
||||
value = kwargs[argname]
|
||||
if isinstance(value, enum.Enum):
|
||||
value = value.value
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
from typing import List
|
||||
import enum
|
||||
|
||||
class HandleType(enum.Enum):
|
||||
AUTO = 'AUTO'
|
||||
VECTOR = 'VECTOR'
|
||||
AUTO_CLAMPED = 'AUTO_CLAMPED'
|
||||
|
||||
class Point:
|
||||
"""
|
||||
A single point on a curve
|
||||
"""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
handle_type: HandleType
|
||||
|
||||
def __init__(self, x: float, y: float, handle_type: HandleType = HandleType.AUTO):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.handle_type = handle_type
|
||||
|
||||
class Curve:
|
||||
"""
|
||||
A class that represents a curve.
|
||||
|
||||
Create a curve from a set of `Point`s.
|
||||
```python
|
||||
my_curve = Curve(
|
||||
Point(0, 0, Handle.AUTO_CLAMPED),
|
||||
Point(0.2, 0.3, Handle.AUTO),
|
||||
Point(1, 1, Handle.VECTOR)
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
points: List[Point]
|
||||
|
||||
def __init__(self, *points: Point):
|
||||
if len(points) == 1 and isinstance(points[0], list):
|
||||
self.points = points[0]
|
||||
else:
|
||||
self.points = list(points)
|
||||
|
||||
def apply(self, curve):
|
||||
"""
|
||||
Apply the points to a curve object.
|
||||
"""
|
||||
for i, point in enumerate(self.points):
|
||||
if len(curve.points) > i:
|
||||
curve.points[i].location = (point.x, point.y)
|
||||
curve.points[i].handle_type = point.handle_type.value
|
||||
else:
|
||||
curve.points.new(point.x, point.y).handle_type = point.handle_type.value
|
|
@ -1,25 +1,46 @@
|
|||
import bpy
|
||||
import inspect
|
||||
import typing
|
||||
|
||||
class SimulationInput:
|
||||
class DeltaTime: pass
|
||||
class ElapsedTime: pass
|
||||
|
||||
def simulation(block: typing.Callable[typing.Any, 'Geometry']):
|
||||
def simulation_zone(block: typing.Callable):
|
||||
"""
|
||||
Create a simulation input/output block.
|
||||
|
||||
> Only available in the `geometry-node-simulation` branch of Blender 3.5.
|
||||
> Only available in Blender 3.6+.
|
||||
"""
|
||||
def wrapped(geometry: 'Geometry', *args, **kwargs):
|
||||
from geometry_script import simulation_input, simulation_output
|
||||
simulation_in = simulation_input(geometry=geometry)
|
||||
def wrapped(*args, **kwargs):
|
||||
from geometry_script.api.node_mapper import OutputsList, set_or_create_link
|
||||
from geometry_script.api.state import State
|
||||
from geometry_script.api.types import Type, socket_class_to_data_type
|
||||
|
||||
signature = inspect.signature(block)
|
||||
for key, value in signature.parameters.items():
|
||||
match value.annotation:
|
||||
case SimulationInput.DeltaTime:
|
||||
kwargs[key] = simulation_in.delta_time
|
||||
case SimulationInput.ElapsedTime:
|
||||
kwargs[key] = simulation_in.elapsed_time
|
||||
return simulation_output(geometry=block(simulation_in.geometry, *args, **kwargs)).geometry
|
||||
|
||||
# setup zone
|
||||
simulation_in = State.current_node_tree.nodes.new(bpy.types.GeometryNodeSimulationInput.__name__)
|
||||
simulation_out = State.current_node_tree.nodes.new(bpy.types.GeometryNodeSimulationOutput.__name__)
|
||||
simulation_in.pair_with_output(simulation_out)
|
||||
|
||||
# clear state items
|
||||
for item in simulation_out.state_items:
|
||||
simulation_out.state_items.remove(item)
|
||||
|
||||
# create state items from block signature
|
||||
state_items = {}
|
||||
for param in [*signature.parameters.values()][1:]:
|
||||
state_items[param.name] = (param.annotation, param.default, None, None)
|
||||
for i, arg in enumerate(state_items.items()):
|
||||
simulation_out.state_items.new(socket_class_to_data_type(arg[1][0].socket_type), arg[0].replace('_', ' ').title())
|
||||
set_or_create_link(kwargs[arg[0]] if arg[0] in kwargs else args[i], simulation_in.inputs[i])
|
||||
|
||||
step = block(*[Type(o) for o in simulation_in.outputs[:-1]])
|
||||
|
||||
if isinstance(step, Type):
|
||||
step = (step,)
|
||||
for i, result in enumerate(step):
|
||||
State.current_node_tree.links.new(result._socket, simulation_out.inputs[i])
|
||||
|
||||
if len(simulation_out.outputs[:-1]) == 1:
|
||||
return Type(simulation_out.outputs[0])
|
||||
else:
|
||||
return OutputsList({o.name.lower().replace(' ', '_'): Type(o) for o in simulation_out.outputs[:-1]})
|
||||
return wrapped
|
42
api/tree.py
42
api/tree.py
|
@ -8,6 +8,7 @@ from .state import State
|
|||
from .types import *
|
||||
from .node_mapper import *
|
||||
from .static.attribute import *
|
||||
from .static.curve import *
|
||||
from .static.expression import *
|
||||
from .static.input_group import *
|
||||
from .static.sample_mode import *
|
||||
|
@ -22,6 +23,9 @@ def _as_iterable(x):
|
|||
except TypeError:
|
||||
return [x,]
|
||||
|
||||
get_node_inputs = lambda x: [i for i in x.interface.items_tree if i.item_type == 'SOCKET' and i.in_out == 'INPUT']
|
||||
get_node_outputs = lambda x: [i for i in x.interface.items_tree if i.item_type == 'SOCKET' and i.in_out == 'OUTPUT']
|
||||
|
||||
def tree(name):
|
||||
tree_name = name
|
||||
def build_tree(builder):
|
||||
|
@ -33,13 +37,20 @@ def tree(name):
|
|||
node_group = bpy.data.node_groups[tree_name]
|
||||
else:
|
||||
node_group = bpy.data.node_groups.new(tree_name, 'GeometryNodeTree')
|
||||
|
||||
node_group.is_modifier = True
|
||||
|
||||
# Clear the node group before building
|
||||
for node in node_group.nodes:
|
||||
node_group.nodes.remove(node)
|
||||
while len(node_group.inputs) > sum(map(lambda p: len(p.annotation.__annotations__) if issubclass(p.annotation, InputGroup) else 1, list(signature.parameters.values()))):
|
||||
node_group.inputs.remove(node_group.inputs[-1])
|
||||
for group_output in node_group.outputs:
|
||||
node_group.outputs.remove(group_output)
|
||||
|
||||
node_inputs = get_node_inputs(node_group)
|
||||
input_count = sum(map(lambda p: len(p.annotation.__annotations__) if issubclass(p.annotation, InputGroup) else 1, list(signature.parameters.values())))
|
||||
for node_input in node_inputs[input_count:]:
|
||||
node_group.interface.remove(node_input)
|
||||
|
||||
for group_output in get_node_outputs(node_group):
|
||||
node_group.interface.remove(group_output)
|
||||
|
||||
# Setup the group inputs
|
||||
group_input_node = node_group.nodes.new('NodeGroupInput')
|
||||
|
@ -64,19 +75,22 @@ def tree(name):
|
|||
inputs[param.name] = (param.annotation, param.default, None, None)
|
||||
|
||||
# Create the input sockets and collect input values.
|
||||
for i, node_input in enumerate(node_group.inputs):
|
||||
node_inputs = get_node_inputs(node_group)
|
||||
for i, node_input in enumerate(node_inputs):
|
||||
if node_input.bl_socket_idname != list(inputs.values())[i][0].socket_type:
|
||||
for ni in node_group.inputs:
|
||||
node_group.inputs.remove(ni)
|
||||
for ni in node_inputs:
|
||||
node_group.interface.remove(ni)
|
||||
break
|
||||
builder_inputs = {}
|
||||
|
||||
node_inputs = get_node_inputs(node_group)
|
||||
for i, arg in enumerate(inputs.items()):
|
||||
input_name = arg[0].replace('_', ' ').title()
|
||||
if len(node_group.inputs) > i:
|
||||
node_group.inputs[i].name = input_name
|
||||
node_input = node_group.inputs[i]
|
||||
if len(node_inputs) > i:
|
||||
node_inputs[i].name = input_name
|
||||
node_input = node_inputs[i]
|
||||
else:
|
||||
node_input = node_group.inputs.new(arg[1][0].socket_type, input_name)
|
||||
node_input = node_group.interface.new_socket(socket_type=arg[1][0].socket_type, name=input_name, in_out='INPUT')
|
||||
if arg[1][1] != inspect.Parameter.empty:
|
||||
node_input.default_value = arg[1][1]
|
||||
if arg[1][2] is not None:
|
||||
|
@ -103,14 +117,14 @@ def tree(name):
|
|||
for i, (k, v) in enumerate(outputs.items()):
|
||||
if not issubclass(type(v), Type):
|
||||
v = Type(value=v)
|
||||
node_group.outputs.new(v.socket_type, k)
|
||||
node_group.interface.new_socket(socket_type=v.socket_type, name=k, in_out='OUTPUT')
|
||||
node_group.links.new(v._socket, group_output_node.inputs[i])
|
||||
else:
|
||||
for i, result in enumerate(_as_iterable(outputs)):
|
||||
if not issubclass(type(result), Type):
|
||||
result = Type(value=result)
|
||||
# raise Exception(f"Return value '{result}' is not a valid 'Type' subclass.")
|
||||
node_group.outputs.new(result.socket_type, 'Result')
|
||||
node_group.interface.new_socket(socket_type=result.socket_type, name='Result', in_out='OUTPUT')
|
||||
node_group.links.new(result._socket, group_output_node.inputs[i])
|
||||
|
||||
_arrange(node_group)
|
||||
|
@ -118,7 +132,7 @@ def tree(name):
|
|||
# Return a function that creates a NodeGroup node in the tree.
|
||||
# This lets @trees be used in other @trees via simple function calls.
|
||||
def group_reference(*args, **kwargs):
|
||||
result = group(node_tree=node_group, *args, **kwargs)
|
||||
result = geometrynodegroup(node_tree=node_group, *args, **kwargs)
|
||||
group_outputs = []
|
||||
for group_output in result._socket.node.outputs:
|
||||
group_outputs.append(Type(group_output))
|
||||
|
|
11
api/types.py
11
api/types.py
|
@ -20,6 +20,15 @@ def socket_type_to_data_type(socket_type):
|
|||
case _:
|
||||
return socket_type
|
||||
|
||||
def socket_class_to_data_type(socket_class_name):
|
||||
match socket_class_name:
|
||||
case 'NodeSocketGeometry':
|
||||
return 'GEOMETRY'
|
||||
case 'NodeSocketFloat':
|
||||
return 'FLOAT'
|
||||
case _:
|
||||
return socket_class_name
|
||||
|
||||
# The base class all exposed socket types conform to.
|
||||
class _TypeMeta(type):
|
||||
def __getitem__(self, args):
|
||||
|
@ -217,6 +226,8 @@ class Type(metaclass=_TypeMeta):
|
|||
return self.transfer_attribute(data_type=data_type, attribute=attribute, **kwargs)
|
||||
|
||||
def __getitem__(self, subscript):
|
||||
if self._socket.type == 'VECTOR' and isinstance(subscript, int):
|
||||
return self._get_xyz_component(subscript)
|
||||
if isinstance(subscript, tuple):
|
||||
accessor = subscript[0]
|
||||
args = subscript[1:]
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
- [Input Groups](./api/advanced-scripting/input-groups.md)
|
||||
- [Attributes](./api/advanced-scripting/attributes.md)
|
||||
- [Boolean Math](./api/advanced-scripting/boolean-math.md)
|
||||
- [Curves](./api/advanced-scripting/curves.md)
|
||||
- [Drivers](./api/advanced-scripting/drivers.md)
|
||||
- [Simulation](./api/advanced-scripting/simulation.md)
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# Curves
|
||||
|
||||
Some nodes, such as *Float Curve* take a curve as a property. You can create a curve with the `Curve` class.
|
||||
|
||||
```python
|
||||
float_curve(
|
||||
mapping=Curve(
|
||||
Point(0, 0),
|
||||
Point(0.5, 0.25),
|
||||
Point(1, 1, HandleType.VECTOR), # Optionally specify a handle type, such as `AUTO`, `VECTOR`, or `AUTO_CLAMPED`.
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
![](./float_curve.png)
|
||||
|
||||
You can also pass the points as a list to `Curve`.
|
||||
|
||||
```python
|
||||
points = [Point(0, 0), Point(1, 1)]
|
||||
float_curve(
|
||||
mapping=Curve(points)
|
||||
)
|
||||
```
|
||||
|
||||
If a node has multiple curve properties, such as the *Vector Curves* node, pass a list of curves to the node.
|
||||
|
||||
```python
|
||||
vector_curves(
|
||||
mapping=[x_curve, y_curve, z_curve]
|
||||
)
|
||||
```
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 36 KiB |
|
@ -1,26 +1,22 @@
|
|||
# Simulation
|
||||
|
||||
> This API is subject to change as future builds of Blender with simulation nodes are released.
|
||||
|
||||
The `geometry-nodes-simulation` branch of Blender 3.5 includes support for "simulation nodes".
|
||||
Blender 3.6 includes simulation nodes.
|
||||
|
||||
Using a *Simulation Input* and *Simulation Output* node, you can create effects that change over time.
|
||||
|
||||
As a convenience, the `@simulation` decorator is provided to make simulation node blocks easier to create.
|
||||
As a convenience, the `@simulation_zone` decorator is provided to make simulation node blocks easier to create.
|
||||
|
||||
```python
|
||||
@simulation
|
||||
def move_over_time(
|
||||
geometry: Geometry, # the first input must be `Geometry`
|
||||
speed: Float,
|
||||
dt: SimulationInput.DeltaTime, # Automatically passes the delta time on any argument annotated with `SimulationInput.DeltaTime`.
|
||||
elapsed: SimulationInput.ElapsedTime, # Automatically passes the elapsed time
|
||||
) -> Geometry:
|
||||
return geometry.set_position(
|
||||
offset=combine_xyz(x=speed)
|
||||
)
|
||||
from geometry_script import *
|
||||
|
||||
@tree
|
||||
def test_sim(geometry: Geometry):
|
||||
@simulation_zone
|
||||
def my_sim(delta_time, geometry: Geometry, value: Float):
|
||||
return (geometry, value)
|
||||
return my_sim(geometry, 0.26).value
|
||||
```
|
||||
|
||||
Every frame the argument `geometry` will be set to the geometry from the previous frame. This allows the offset to accumulate over time.
|
||||
|
||||
The `SimulationInput.DeltaTime`/`SimulationInput.ElapsedTime` types mark arguments that should be given the outputs from the *Simulation Input* node.
|
||||
The first argument should always be `delta_time`. Any other arguments must also be returned as a tuple with their modified values.
|
||||
Each frame, the result from the previous frame is passed into the zone's inputs.
|
||||
The initial call to `my_sim` in `test_sim` provides the initial values for the simulation.
|
Ładowanie…
Reference in New Issue