From 6831b609c3fd53a79172dc3e4e081e5f7a8fa280 Mon Sep 17 00:00:00 2001 From: Hans Goudey Date: Tue, 14 Feb 2023 17:21:55 -0500 Subject: [PATCH 1/4] Curves: Replace quick fur operator, add to add menu The quick fur operator now uses the new hair system. It adds a new curves object for every selected mesh, and adds geometry nodes modifiers from the essentials assets that generate curves. A few settings are exposed in the redo panel, including an option for whether to apply the modifier to generate the initial curves so that there is original editable data. The point of the operator is to give people a sense of how to use the node groups and to give a very fast way to build a basic setup for further tweaking. --- .../bl_operators/object_quick_effects.py | 135 +++++++++++++----- release/scripts/startup/bl_ui/space_view3d.py | 1 + 2 files changed, 102 insertions(+), 34 deletions(-) diff --git a/release/scripts/startup/bl_operators/object_quick_effects.py b/release/scripts/startup/bl_operators/object_quick_effects.py index d8b0e85be0e..4bc02dd503a 100644 --- a/release/scripts/startup/bl_operators/object_quick_effects.py +++ b/release/scripts/startup/bl_operators/object_quick_effects.py @@ -36,67 +36,134 @@ class ObjectModeOperator: class QuickFur(ObjectModeOperator, Operator): - """Add fur setup to the selected objects""" + """Add a fur setup to the selected objects""" bl_idname = "object.quick_fur" bl_label = "Quick Fur" bl_options = {'REGISTER', 'UNDO'} density: EnumProperty( - name="Fur Density", + name="Density", items=( - ('LIGHT', "Light", ""), + ('LOW', "Low", ""), ('MEDIUM', "Medium", ""), - ('HEAVY', "Heavy", ""), + ('HIGH', "High", ""), ), default='MEDIUM', ) - view_percentage: IntProperty( - name="View %", - min=1, max=100, - soft_min=1, soft_max=100, - default=10, - ) length: FloatProperty( name="Length", min=0.001, max=100, soft_min=0.01, soft_max=10, default=0.1, + subtype='DISTANCE' + ) + radius: FloatProperty( + name="Hair Radius", + min=0.0, max=10, + soft_min=0.0001, soft_max=0.1, + default=0.001, + subtype='DISTANCE' + ) + view_percentage: FloatProperty( + name="View Percentage", + min=0.0, max=1.0, + default=1.0, + subtype='FACTOR' + ) + keep_hair_guides: BoolProperty( + name="Keep Hair Guides", + default=False, + ) + use_noise: BoolProperty( + name="Noise", + default=True, + ) + use_frizz: BoolProperty( + name="Frizz", + default=True, ) def execute(self, context): - fake_context = context.copy() - mesh_objects = [obj for obj in context.selected_objects - if obj.type == 'MESH'] - + import os + mesh_objects = [obj for obj in context.selected_objects if obj.type == 'MESH'] if not mesh_objects: self.report({'ERROR'}, "Select at least one mesh object") return {'CANCELLED'} - mat = bpy.data.materials.new("Fur Material") + if self.density == 'LOW': + count = 1000 + elif self.density == 'MEDIUM': + count = 10000 + elif self.density == 'HIGH': + count = 100000 - for obj in mesh_objects: - fake_context["object"] = obj - bpy.ops.object.particle_system_add(fake_context) + node_groups_to_append = {"Generate Hair Curves", "Set Hair Curve Profile", "Interpolate Hair Curves"} + if self.use_noise: + node_groups_to_append.add("Hair Curves Noise") + if self.use_frizz: + node_groups_to_append.add("Frizz Hair Curves") + assets_directory = os.path.join(bpy.utils.system_resource('DATAFILES'), + "assets", + "geometry_nodes", + "procedural_hair_node_assets.blend", + "NodeTree") + for name in node_groups_to_append: + bpy.ops.wm.append(directory=assets_directory, + filename=name, + use_recursive=True, + do_reuse_local_id=True) + generate_group = bpy.data.node_groups["Generate Hair Curves"] + interpolate_group = bpy.data.node_groups["Interpolate Hair Curves"] + radius_group = bpy.data.node_groups["Set Hair Curve Profile"] + noise_group = bpy.data.node_groups["Hair Curves Noise"] if self.use_noise else None + frizz_group = bpy.data.node_groups["Frizz Hair Curves"] if self.use_frizz else None - psys = obj.particle_systems[-1] - psys.settings.type = 'HAIR' + material = bpy.data.materials.new("Fur Material") - if self.density == 'LIGHT': - psys.settings.count = 100 - elif self.density == 'MEDIUM': - psys.settings.count = 1000 - elif self.density == 'HEAVY': - psys.settings.count = 10000 + for mesh_object in mesh_objects: + mesh = mesh_object.data + with context.temp_override(active_object=mesh_object): + bpy.ops.object.curves_empty_hair_add() + curves_object = context.active_object + curves = curves_object.data + curves.materials.append(material) - psys.settings.child_nbr = self.view_percentage - psys.settings.hair_length = self.length - psys.settings.use_strand_primitive = True - psys.settings.use_hair_bspline = True - psys.settings.child_type = 'INTERPOLATED' - psys.settings.tip_radius = 0.25 + area = 0.0 + for poly in mesh.polygons: + area += poly.area + density = count / area - obj.data.materials.append(mat) - psys.settings.material = len(obj.data.materials) + generate_modifier = curves_object.modifiers.new(name="Generate", type='NODES') + generate_modifier.node_group = generate_group + generate_modifier["Input_2"] = mesh_object + generate_modifier["Input_18_attribute_name"] = curves.surface_uv_map + generate_modifier["Input_20"] = self.length + generate_modifier["Input_22"] = material + generate_modifier["Input_15"] = density * 0.01 + curves_object.modifiers.move(1, 0) + + radius_modifier = curves_object.modifiers.new(name="Set Hair Curve Profile", type='NODES') + radius_modifier.node_group = radius_group + radius_modifier["Input_3"] = self.radius + + interpolate_modifier = curves_object.modifiers.new(name="Interpolate Hair Curves", type='NODES') + interpolate_modifier.node_group = interpolate_group + interpolate_modifier["Input_2"] = mesh_object + interpolate_modifier["Input_18_attribute_name"] = curves.surface_uv_map + interpolate_modifier["Input_15"] = density + interpolate_modifier["Input_17"] = self.view_percentage + + if noise_group: + noise_modifier = curves_object.modifiers.new(name="Hair Curves Noise", type='NODES') + noise_modifier.node_group = noise_group + + if frizz_group: + frizz_modifier = curves_object.modifiers.new(name="Frizz Hair Curves", type='NODES') + frizz_modifier.node_group = frizz_group + + if not self.keep_hair_guides: + with context.temp_override(object=curves_object): + bpy.ops.object.modifier_apply(modifier=generate_modifier.name) return {'FINISHED'} diff --git a/release/scripts/startup/bl_ui/space_view3d.py b/release/scripts/startup/bl_ui/space_view3d.py index a1548741187..3b5e46f2e0d 100644 --- a/release/scripts/startup/bl_ui/space_view3d.py +++ b/release/scripts/startup/bl_ui/space_view3d.py @@ -2116,6 +2116,7 @@ class VIEW3D_MT_curve_add(Menu): layout.separator() layout.operator("object.curves_empty_hair_add", text="Empty Hair", icon='CURVES_DATA') + layout.operator("object.quick_fur", text="Fur", icon='CURVES_DATA') experimental = context.preferences.experimental if experimental.use_new_curves_tools: -- 2.30.2 From 88aabf443f260ff6fe5549071f49fe13060d8464 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Wed, 15 Feb 2023 11:20:45 +0100 Subject: [PATCH 2/4] rename Keep Hair Guides to Apply Hair Guides --- .../scripts/startup/bl_operators/object_quick_effects.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/release/scripts/startup/bl_operators/object_quick_effects.py b/release/scripts/startup/bl_operators/object_quick_effects.py index 4bc02dd503a..48663226883 100644 --- a/release/scripts/startup/bl_operators/object_quick_effects.py +++ b/release/scripts/startup/bl_operators/object_quick_effects.py @@ -70,8 +70,8 @@ class QuickFur(ObjectModeOperator, Operator): default=1.0, subtype='FACTOR' ) - keep_hair_guides: BoolProperty( - name="Keep Hair Guides", + apply_hair_guides: BoolProperty( + name="Apply Hair Guides", default=False, ) use_noise: BoolProperty( @@ -161,7 +161,7 @@ class QuickFur(ObjectModeOperator, Operator): frizz_modifier = curves_object.modifiers.new(name="Frizz Hair Curves", type='NODES') frizz_modifier.node_group = frizz_group - if not self.keep_hair_guides: + if self.apply_hair_guides: with context.temp_override(object=curves_object): bpy.ops.object.modifier_apply(modifier=generate_modifier.name) -- 2.30.2 From 953a076fa22ee6b9783b0edda3750a0a3fe2c8e6 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Wed, 15 Feb 2023 11:23:31 +0100 Subject: [PATCH 3/4] enable follow surface normal --- release/scripts/startup/bl_operators/object_quick_effects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/release/scripts/startup/bl_operators/object_quick_effects.py b/release/scripts/startup/bl_operators/object_quick_effects.py index 48663226883..edc7b2c9a53 100644 --- a/release/scripts/startup/bl_operators/object_quick_effects.py +++ b/release/scripts/startup/bl_operators/object_quick_effects.py @@ -152,6 +152,7 @@ class QuickFur(ObjectModeOperator, Operator): interpolate_modifier["Input_18_attribute_name"] = curves.surface_uv_map interpolate_modifier["Input_15"] = density interpolate_modifier["Input_17"] = self.view_percentage + interpolate_modifier["Input_24"] = True if noise_group: noise_modifier = curves_object.modifiers.new(name="Hair Curves Noise", type='NODES') -- 2.30.2 From f82531f8222acf2ce485c156e7deb4bc9547dbda Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Wed, 15 Feb 2023 11:25:20 +0100 Subject: [PATCH 4/4] apply hair guides by default --- release/scripts/startup/bl_operators/object_quick_effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/scripts/startup/bl_operators/object_quick_effects.py b/release/scripts/startup/bl_operators/object_quick_effects.py index edc7b2c9a53..06f44336972 100644 --- a/release/scripts/startup/bl_operators/object_quick_effects.py +++ b/release/scripts/startup/bl_operators/object_quick_effects.py @@ -72,7 +72,7 @@ class QuickFur(ObjectModeOperator, Operator): ) apply_hair_guides: BoolProperty( name="Apply Hair Guides", - default=False, + default=True, ) use_noise: BoolProperty( name="Noise", -- 2.30.2