Skip to content
Snippets Groups Projects
add_mesh_3d_function_surface.py 18.2 KiB
Newer Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

import bpy
from Mathutils import *
from math import *
from bpy.props import *

Brendon Murphy's avatar
Brendon Murphy committed
bl_addon_info = {
    'name': 'Add Mesh: 3D Function Surfaces',
Brendon Murphy's avatar
Brendon Murphy committed
    'author': 'Buerbaum Martin (Pontiac)',
    'version': '0.3.2',
    'blender': (2, 5, 3),
    'location': 'View3D > Add > Mesh > Z Function Surface &' \
        ' XYZ Function Surface',
    'description': 'Create Objects using Math Formulas',
    'url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \
        'Scripts/Add_Mesh/Add_3d_Function_Surface',
Brendon Murphy's avatar
Brendon Murphy committed
    'category': 'Add Mesh'}

# More Links:
# http://gitorious.org/blender-scripts/blender-3d-function-surface
Brendon Murphy's avatar
Brendon Murphy committed
# http://blenderartists.org/forum/showthread.php?t=179043

__bpydoc__ = """
Z Function Surface

This script lets the user create a surface where the z coordinate
is a function of the x and y coordinates.

    z = f(x,y)

X,Y,Z Function Surface

This script lets the user create a surface where the x, y and z
coordinates are defiend by a function.

    x = f(u,v)
    y = f(u,v)
    z = f(u,v)

Usage:
You have to activated the script in the "Add-Ons" tab (user preferences).
The functionality can then be accessed via the
"Add Mesh" -> "Z Function Surface"
and
"Add Mesh" -> "X,Y,Z Function Surface"
menu.

Version history:
v0.3.2 - Various fixes&streamlining by ideasman42/Campbell Barton.
    r544 Compile expressions for faster execution
    r544 Use operator reports for errors too
    r544 Avoid type checks by converting to a float, errors
    converting to a float are reported too.
    Fixed an error Campbell overlooked (appending tuples to an
    array, not single values) Thamnks for the report wild_doogy.
    Added 'description' field, updated 'url'.
    Made the script PEP8 compatible again.
Martin Buerbaum's avatar
Martin Buerbaum committed
v0.3.1 - Use hidden "edit" property for "recall" operator.
    Bugfix: Z Function was mixing up div_x and div_y
v0.3 - X,Y,Z Function Surface (by Ed Mackey & tuga3d).
    Renamed old function to "Z Function Surface".
    Align the geometry to the view if the user preference says so.
    Store recall properties in newly created object.
Brendon Murphy's avatar
Brendon Murphy committed
v0.2.3 - Use bl_addon_info for Add-On information.
v0.2.2 - Fixed Add-On registration text.
v0.2.1 - Fixed some new API stuff.
    Mainly we now have the register/unregister functions.
    Also the new() function for objects now accepts a mesh object.
    Changed the script so it can be managed from the "Add-Ons" tab
    in the user preferences.
    Added dummy "PLUGIN" icon.
    Corrected FSF address.
    Clean up of tooltips.
v0.2 - Added security check for eval() function
    Check return value of eval() for complex numbers.
v0.1.1 - Use 'CANCELLED' return value when failing.
    Updated web links.
v0.1 - Initial revision.
"""

# List of safe functions for eval()
safe_list = ['math', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'cosh',
    'degrees', 'e', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'hypot',
    'ldexp', 'log', 'log10', 'modf', 'pi', 'pow', 'radians',
    'sin', 'sinh', 'sqrt', 'tan', 'tanh']

# Use the list to filter the local namespace
safe_dict = dict([(k, globals().get(k, None)) for k in safe_list])


# Stores the values of a list of properties in a
# property group (named like the operator) in the object.
# Always replaces any existing property group with the same name!
# @todo: Should we do this in EDIT Mode? Sounds dangerous.
def obj_store_recall_properties(ob, op, prop_list):
    if ob and op and prop_list:
        #print("Storing recall data for operator: " + op.bl_idname)  # DEBUG

        # Store new recall properties.
        prop_list['recall_op'] = op.bl_idname
        ob['recall'] = prop_list

# Apply view rotation to objects if "Align To" for new objects
# was set to "VIEW" in the User Preference.
def apply_view_rotation(context, ob):
    align = bpy.context.user_preferences.edit.object_align

    if (context.space_data.type == 'VIEW_3D'
        and align == 'VIEW'):
            view3d = context.space_data
            region = view3d.region_3d
            viewMatrix = region.view_matrix
            rot = viewMatrix.rotation_part()
            ob.rotation_euler = rot.invert().to_euler()


def createFaces(vertIdx1, vertIdx2, ring):
    '''
    A very simple "bridge" tool.
    Connects two equally long vertex-loops with faces and
    returns a list of the new faces.

    Parameters
        vertIdx1 ... List of vertex indices of the first loop.
        vertIdx2 ... List of vertex indices of the second loop.
    '''
    faces = []

    if (len(vertIdx1) != len(vertIdx2)) or (len(vertIdx1) < 2):
        return None

    total = len(vertIdx1)

    if (ring):
        # Bridge the start with the end.
        faces.append([vertIdx2[0], vertIdx1[0],
            vertIdx1[total - 1], vertIdx2[total - 1]])

    # Bridge the rest of the faces.
    for num in range(total - 1):
        faces.append([vertIdx1[num], vertIdx2[num],
            vertIdx2[num + 1], vertIdx1[num + 1]])

    return faces


Martin Buerbaum's avatar
Martin Buerbaum committed
def createObject(context, verts, faces, name, edit):
    '''Creates Meshes & Objects for the given lists of vertices and faces.'''

    scene = context.scene

    # Create new mesh
    mesh = bpy.data.meshes.new(name)

    # Add the geometry to the mesh.
    #mesh.add_geometry(len(verts), 0, len(faces))
    #mesh.verts.foreach_set("co", unpack_list(verts))
    #mesh.faces.foreach_set("verts_raw", unpack_face_list(faces))

    # To quote the documentation:
    # "Make a mesh from a list of verts/edges/faces Until we have a nicer
    #  way to make geometry, use this."
    # http://www.blender.org/documentation/250PythonDoc/
    # bpy.types.Mesh.html#bpy.types.Mesh.from_pydata
    mesh.from_pydata(verts, [], faces)

Martin Buerbaum's avatar
Martin Buerbaum committed
    # Deselect all objects.
    bpy.ops.object.select_all(action='DESELECT')

    # Update mesh geometry after adding stuff.
    mesh.update()

Martin Buerbaum's avatar
Martin Buerbaum committed
    if edit:
        # Recreate geometry of existing object
        obj_act = context.active_object
        ob_new = obj_act
Martin Buerbaum's avatar
Martin Buerbaum committed
        bpy.ops.object.mode_set(mode='EDIT')
        bpy.ops.mesh.select_all(action='SELECT')
        bpy.ops.mesh.delete(type='VERT')
        bpy.ops.object.mode_set(mode='OBJECT')
Martin Buerbaum's avatar
Martin Buerbaum committed
        ob_new.data = mesh
Martin Buerbaum's avatar
Martin Buerbaum committed
    else:
        # Create new object
        ob_new = bpy.data.objects.new(name, mesh)
Martin Buerbaum's avatar
Martin Buerbaum committed
        # Link new object to the given scene and select it.
        scene.objects.link(ob_new)
        ob_new.selected = True
Martin Buerbaum's avatar
Martin Buerbaum committed
        # Place the object at the 3D cursor location.
        ob_new.location = scene.cursor_location
Martin Buerbaum's avatar
Martin Buerbaum committed
        obj_act = scene.objects.active
Martin Buerbaum's avatar
Martin Buerbaum committed
        apply_view_rotation(context, ob_new)
    if obj_act and obj_act.mode == 'EDIT':
Martin Buerbaum's avatar
Martin Buerbaum committed
        if not edit:
            # We are in EditMode, switch to ObjectMode.
            bpy.ops.object.mode_set(mode='OBJECT')
Martin Buerbaum's avatar
Martin Buerbaum committed
            # Select the active object as well.
            obj_act.selected = True
Martin Buerbaum's avatar
Martin Buerbaum committed
            # Apply location of new object.
            scene.update()
Martin Buerbaum's avatar
Martin Buerbaum committed
            # Join new object into the active.
            bpy.ops.object.join()
Martin Buerbaum's avatar
Martin Buerbaum committed
            # Switching back to EditMode.
            bpy.ops.object.mode_set(mode='EDIT')

    else:
        # We are in ObjectMode.
        # Make the new object the active one.
        scene.objects.active = ob_new

    return ob_new

class AddZFunctionSurface(bpy.types.Operator):
    '''Add a surface defined defined by a function z=f(x,y)'''
    bl_idname = "mesh.primitive_z_function_surface"
    bl_label = "Add Z Function Surface"
    bl_options = {'REGISTER', 'UNDO'}

Martin Buerbaum's avatar
Martin Buerbaum committed
    # edit - Whether to add or update.
    edit = BoolProperty(name="",
Martin Buerbaum's avatar
Martin Buerbaum committed
        description="",
Martin Buerbaum's avatar
Martin Buerbaum committed
        default=False,
        options={'HIDDEN'})

    equation = StringProperty(name="Z Equation",
        description="Equation for z=f(x,y)",
        default="1 - ( x**2 + y**2 )")

    div_x = IntProperty(name="X Subdivisions",
        description="Number of vertices in x direction.",
        default=16,
        min=3,
        max=256)
    div_y = IntProperty(name="Y Subdivisions",
        description="Number of vertices in y direction.",
        default=16,
        min=3,
        max=256)

    size_x = FloatProperty(name="X Size",
        description="Size of the x axis.",
        default=2.0,
        min=0.01,
        max=100.0,
        unit="LENGTH")
    size_y = FloatProperty(name="Y Size",
        description="Size of the y axis.",
        default=2.0,
        min=0.01,
        max=100.0,
        unit="LENGTH")

    def execute(self, context):
Martin Buerbaum's avatar
Martin Buerbaum committed
        edit = self.properties.edit
        equation = self.properties.equation
        div_x = self.properties.div_x
        div_y = self.properties.div_y
        size_x = self.properties.size_x
        size_y = self.properties.size_y

        verts = []
        faces = []

        delta_x = size_x / float(div_x - 1)
        delta_y = size_y / float(div_y - 1)
        start_x = -(size_x / 2.0)
        start_y = -(size_y / 2.0)

        edgeloop_prev = []

            expr_args = (
                compile(equation, __file__, 'eval'),
                {"__builtins__": None},
                safe_dict)
        except:
            import traceback
            self.report({'ERROR'}, "Error parsing expression: "
                + traceback.format_exc(limit=1))
            return {'CANCELLED'}

        for row_x in range(div_x):
            edgeloop_cur = []
            x = start_x + row_x * delta_x

            for row_y in range(div_y):
                y = start_y + row_y * delta_y
                z = 0.0

                safe_dict['x'] = x
                safe_dict['y'] = y

                # Try to evaluate the equation.
                try:
                    z = float(eval(*expr_args))
                except:
                    import traceback
                    self.report({'ERROR'}, "Error evaluating expression: "
                        + traceback.format_exc(limit=1))
                    return {'CANCELLED'}

                edgeloop_cur.append(len(verts))
                verts.append((x, y, z))

            if len(edgeloop_prev) > 0:
                faces_row = createFaces(edgeloop_prev, edgeloop_cur, False)
                faces.extend(faces_row)

            edgeloop_prev = edgeloop_cur

Martin Buerbaum's avatar
Martin Buerbaum committed
        obj = createObject(context, verts, faces, "Z Function", edit)

        # Store 'recall' properties in the object.
        recall_prop_list = {
Martin Buerbaum's avatar
Martin Buerbaum committed
            "edit": True,
            "equation": equation,
            "div_x": div_x,
Martin Buerbaum's avatar
Martin Buerbaum committed
            "div_y": div_y,
            "size_x": size_x,
            "size_y": size_y}
        obj_store_recall_properties(obj, self, recall_prop_list)

        return {'FINISHED'}


def xyz_function_surface_faces(self, x_eq, y_eq, z_eq,
    range_u_min, range_u_max, range_u_step, wrap_u,
    range_v_min, range_v_max, range_v_step, wrap_v):

    verts = []
    faces = []

    uStep = (range_u_max - range_u_min) / range_u_step
    vStep = (range_v_max - range_v_min) / range_v_step

    uRange = range_u_step
    if range_u_step == 0:
        uRange = uRange + 1

    vRange = range_v_step
    if range_v_step == 0:
        vRange = vRange + 1

        expr_args_x = (
            compile(x_eq, __file__.replace(".py", "_x.py"), 'eval'),
            {"__builtins__": None},
            safe_dict)
        expr_args_y = (
            compile(y_eq, __file__.replace(".py", "_y.py"), 'eval'),
            {"__builtins__": None},
            safe_dict)
        expr_args_z = (
            compile(z_eq, __file__.replace(".py", "_z.py"), 'eval'),
            {"__builtins__": None},
            safe_dict)
    except:
        import traceback
        self.report({'ERROR'}, "Error parsing expression: "
            + traceback.format_exc(limit=1))
    for vN in range(vRange):
        v = range_v_min + (vN * vStep)

        for uN in range(uRange):
            u = range_u_min + (uN * uStep)

            safe_dict['u'] = u
            safe_dict['v'] = v

            # Try to evaluate the equation.
            try:
                verts.append((
                    float(eval(*expr_args_x)),
                    float(eval(*expr_args_y)),
                    float(eval(*expr_args_z))))

            except:
                import traceback
                self.report({'ERROR'}, "Error evaluating expression: "
                    + traceback.format_exc(limit=1))

    for vN in range(1, range_v_step + 1):
        vThis = vN

        if (vThis >= vRange):
            if wrap_v:
                vThis = 0
            else:
                continue

        for uN in range(1, range_u_step + 1):
            uThis = uN

            if (uThis >= uRange):
                if wrap_u:
                    uThis = 0
                else:
                    continue

            faces.append([(vThis * uRange) + uThis,
                (vThis * uRange) + uN - 1,
                ((vN - 1) * uRange) + uN - 1,
                ((vN - 1) * uRange) + uThis])

    return verts, faces


# Original Script "Parametric.py" by Ed Mackey.
# -> http://www.blinken.com/blender-plugins.php
# Partly converted for Blender 2.5 by tuga3d.
#
# Sphere:
# x = sin(2*pi*u)*sin(pi*v)
# y = cos(2*pi*u)*sin(pi*v)
# z = cos(pi*v)
# u_min = v_min = 0
# u_max = v_max = 1
#
# "Snail shell"
# x = 1.2**v*(sin(u)**2 *sin(v))
# y = 1.2**v*(sin(u)*cos(u))
# z = 1.2**v*(sin(u)**2 *cos(v))
# u_min = 0
# u_max = pi
# v_min = -pi/4,
# v max = 5*pi/2
class AddXYZFunctionSurface(bpy.types.Operator):
    '''Add a surface defined defined by 3 functions:''' \
    + ''' x=f(u,v), y=f(u,v) and z=f(u,v)'''
    bl_idname = "mesh.primitive_xyz_function_surface"
    bl_label = "Add X,Y,Z Function Surface"
    bl_options = {'REGISTER', 'UNDO'}

Martin Buerbaum's avatar
Martin Buerbaum committed
    # edit - Whether to add or update.
    edit = BoolProperty(name="",
Martin Buerbaum's avatar
Martin Buerbaum committed
        description="",
Martin Buerbaum's avatar
Martin Buerbaum committed
        default=False,
        options={'HIDDEN'})

    x_eq = StringProperty(name="X Equation",
        description="Equation for x=f(u,v)",
        default="1.2**v*(sin(u)**2 *sin(v))")

    y_eq = StringProperty(name="Y Equation",
        description="Equation for y=f(u,v)",
        default="1.2**v*(sin(u)*cos(u))")

    z_eq = StringProperty(name="Z Equation",
        description="Equation for z=f(u,v)",
        default="1.2**v*(sin(u)**2 *cos(v))")

    range_u_min = FloatProperty(name="U min",
        description="Minimum U value. Lower boundary of U range.",
        min=-100.00,
        max=0.00,
        default=0.00)

    range_u_max = FloatProperty(name="U max",
        description="Maximum U value. Upper boundary of U range.",
        min=0.00,
        max=100.00,
        default=pi)

    range_u_step = IntProperty(name="U step",
        description="U Subdivisions",
        min=1,
        max=1024,
        default=32)

    wrap_u = BoolProperty(name="U wrap",
        description="U Wrap around",
        default=True)

    range_v_min = FloatProperty(name="V min",
        description="Minimum V value. Lower boundary of V range.",
        min=-100.00,
        max=0.00,
        default=-pi / 4)

    range_v_max = FloatProperty(name="V max",
        description="Maximum V value. Upper boundary of V range.",
        min=0.00,
        max=100.00,
        default=5 * pi / 2)

    range_v_step = IntProperty(name="V step",
        description="V Subdivisions",
        min=1,
        max=1024,
        default=32)

    wrap_v = BoolProperty(name="V wrap",
        description="V Wrap around",
        default=False)

    def execute(self, context):
        props = self.properties

        verts, faces = xyz_function_surface_faces(
                            props.x_eq,
                            props.y_eq,
                            props.z_eq,
                            props.range_u_min,
                            props.range_u_max,
                            props.range_u_step,
                            props.wrap_u,
                            props.range_v_min,
                            props.range_v_max,
                            props.range_v_step,
                            props.wrap_v)

        if not verts:
            return {'CANCELLED'}

Martin Buerbaum's avatar
Martin Buerbaum committed
        obj = createObject(context, verts, faces, "XYZ Function", props.edit)

        # Store 'recall' properties in the object.
        recall_prop_list = {
Martin Buerbaum's avatar
Martin Buerbaum committed
            "edit": True,
            "x_eq": props.x_eq,
            "y_eq": props.y_eq,
            "z_eq": props.z_eq,
            "range_u_min": props.range_u_min,
            "range_u_max": props.range_u_max,
            "range_u_step": props.range_u_step,
            "wrap_u": props.wrap_u,
            "range_v_min": props.range_v_min,
            "range_v_max": props.range_v_max,
            "range_v_step": props.range_v_step,
            "wrap_v": props.wrap_v}
        obj_store_recall_properties(obj, self, recall_prop_list)

        return {'FINISHED'}


################################
import space_info

# Define "3D Function Surface" menu
menu_func_z = (lambda self, context: self.layout.operator(
    AddZFunctionSurface.bl_idname,
    text="Z Function Surface",
    icon="PLUGIN"))
menu_func_xyz = (lambda self, context: self.layout.operator(
    AddXYZFunctionSurface.bl_idname,
    text="X,Y,Z Function Surface",
    icon="PLUGIN"))


def register():
    # Register the operators/menus.
    bpy.types.register(AddZFunctionSurface)
    bpy.types.register(AddXYZFunctionSurface)
    # Add menus to the "Add Mesh" menu
    space_info.INFO_MT_mesh_add.append(menu_func_z)
    space_info.INFO_MT_mesh_add.append(menu_func_xyz)


def unregister():
    # Unregister the operators/menus.
    bpy.types.unregister(AddZFunctionSurface)
    bpy.types.unregister(AddXYZFunctionSurface)
    # Remove menus from the "Add Mesh" menu.
    space_info.INFO_MT_mesh_add.remove(menu_func_z)
    space_info.INFO_MT_mesh_add.remove(menu_func_xyz)

if __name__ == "__main__":
    register()