3
$\begingroup$

I am attempting to create my own transfer function.

So first I draw a curve:

3D View > View > Top

Info > Add > Curve > Bezier

Tab to enter Edit Mode

Curve Display > Normals > (off)

Properties > (Curve icon) > Shape > 2D

Create the curve using:

  • RMB to select waypoints or control points
  • G to move them,
  • Select last waypoint, E to 'extrude' i.e. add another waypoint,
    or
  • Select two adjacent waypoints using RMB and ShiftRMB > W >

So now I have my curve:

enter image description here

How do I now, from a Python script, retrieve the associated y-value for a given x-value?

P.S: This question follows from: Create custom graph and access values from Python but I realised that trying to hijack the keyframe animation curve editor for this purpose is trying to get a square peg into a round hole.

$\endgroup$
  • $\begingroup$ I think your asking the wrong question. Any number of curve points can have the same x value, you want the y value from the same point that you get the x value. That should entail changing .x to .y or [0] to [1] $\endgroup$ – sambler Apr 11 '14 at 5:16
  • $\begingroup$ This works a little bit differently from graphing in math class. There has been no formula defined for that shape you've created. You can't get 'y' by plugging 'x' into a formula, because no formula has been generated. Besides, both 'x' and 'y' have already have been defined when Blender created each point so you'd be calculating something which is already been calculated and stored in memory. Memory access is much faster than recalculating. $\endgroup$ – MarcClintDion Apr 14 '14 at 6:02
4
$\begingroup$

The following script uses the formula for cubic Bezier curves (which Blender approximates) to first determine t (which is the parametrization of the curve) from x and then gets y from t.

import bpy
import numpy as np

def main():
    '''Here's an example usage of these functions'''    
    x = 5.0    
    bez_obj = bpy.data.objects['BezierCurve']

    print('The y-value is', getYfromXforBezierObject(bez_obj, x))



def getYfromXforBezierObject(bez_obj, x):
    '''Given a Bezier object which, when projected on to xy-plane, is a 
    well-behaved function (i.e. each x-value has only one associated y-value),
    this returns the y-value for a given x-value

    The higher resolution your curve is, the more accurate the resultant y-value
    will be.'''

    #find appropriate segment
    segment_i = determineBezSegment(bez_obj, x)

    if len(bez_obj.data.splines) > 1:
        print("WARNING: your Bezier object has multiple splines!")

    spline = bez_obj.data.splines[0]

    #get the four points that control the cubic bezier segment
    P0 = spline.bezier_points[segment_i].co[:2]
    P1 = spline.bezier_points[segment_i].handle_right[:2]
    P2 = spline.bezier_points[segment_i+1].handle_left[:2]
    P3 = spline.bezier_points[segment_i+1].co[:2]  

    y = getYfromXforBezSegment(P0, P1, P2, P3, x)

    return y


def determineBezSegment(bez_obj, x):
    '''Given a Bezier object and an x-value, determine which of the cubic 
    segments corresponds to that x-value.
    A return value of 0 would represent the first segment.'''
    print(bez_obj.data)
    bez_points = bez_obj.data.splines[0].bezier_points

    if len(bez_obj.data.splines) > 1:
        print("WARNING: your Bezier object has multiple splines!")

    #loop through all segments and find which one contains x
    for segment_i in range(len(bez_points)-1):
        start_point = bez_points[segment_i]
        end_point = bez_points[segment_i+1]
        #break if this segment contains x
        if start_point.co[0] <= x < end_point.co[0]:
            break
    #check if no segment was found
    else: 
        print("WARNING: no segment found for given x-value and Bezier object")
        return None

    return segment_i


def getYfromXforBezSegment(P0, P1, P2, P3, x):
    '''For a cubic Bezier segment described by the 2-tuples P0, ..., P3, return
    the y-value associated with the given x-value.

    Ex: getXfromYforCubicBez((0,0), (1,1), (2,1), (2,2), 3.2)'''

    #First, get the t-value associated with x-value, where t is the
    #parameterization of the Bezier curve and ranges from 0 to 1.
    #We need the coefficients of the polynomial describing cubic Bezier
    #(cubic polynomial in t)
    coefficients = [-P0[0] + 3*P1[0] - 3*P2[0] + P3[0],
                    3*P0[0] - 6*P1[0] + 3*P2[0],
                    -3*P0[0] + 3*P1[0],
                    P0[0] - x]
    #find roots of this polynomial to determine the parameter t
    roots = np.roots(coefficients)
    #find the root which is between 0 and 1, and is also real
    correct_root = None
    for root in roots:
        if np.isreal(root) and 0 <= root <= 1:
            correct_root = root

    #check to make sure a valid root was found
    if correct_root is None:
        print('Error, no valid root found. Are you sure your Bezier curve '
              'represents a valid function when projected into the xy-plane?')
    param_t = correct_root

    #from our value for the t parameter, find the corresponding y-value using formula for
    #cubic Bezier curves
    y = (1-param_t)**3*P0[1] + 3*(1-param_t)**2*param_t*P1[1] + 3*(1-param_t)*param_t**2*P2[1] + param_t**3*P3[1]
    assert np.isreal(y)
    # typecast y from np.complex128 to float64   
    y = y.real
    return y


if __name__ == '__main__':
    main()

This script will work only if your Bezier curve is a function when projected into the xy-plane (hit NUM7 and make sure it passes the "vertical line test").

$\endgroup$
  • $\begingroup$ Blender 2.70 should actually come with NumPy on all platforms. Correct me if it's not the case for Mac OSX, but the Windows builds definately ship with the numpy size-package. $\endgroup$ – CodeManX Apr 11 '14 at 10:41
  • $\begingroup$ Thanks Garrett, this is fantastic! I'm trying to put this function in its own module blender.stackexchange.com/questions/8496/… $\endgroup$ – P i Apr 11 '14 at 14:09
  • $\begingroup$ @Garrett Just a thought. Using the letter 't' as a variable makes the code difficult to follow and makes searching very difficult since the 't' comes up in a lot of words. $\endgroup$ – MarcClintDion Apr 14 '14 at 6:06
3
$\begingroup$

There is a utility function to interpolate a bezier spline segment:

from mathutils.geometry import interpolate_bezier

API docs: mathutils.geometry.interpolate_bezier

$\endgroup$
  • $\begingroup$ I can't see how to throw in an x and get out a y. It looks as though I have to throw in a t and get out an xyz. In which case I would need to write quite a messy algorithm to repeatedly bisect t depending on whether the x from the returned xyz is greater or lesser than the target x $\endgroup$ – P i Apr 10 '14 at 20:16
  • $\begingroup$ Actually, it can be solved analytically although it might be a huge mess. This is because Blender uses cubic Bezier curves and cubic polynomials can be solved analytically. $\endgroup$ – Garrett Apr 11 '14 at 9:30
  • $\begingroup$ @Anyone, feel free to correct me if I'm wrong: 't' is probably an array of addresses which means that t has many values;t[0], t[1], t[2], etc,... For a curve with 3 points it might help to think of 't' as being a container that holds 3 more smaller containers, one for each point and each small container has 3 slots, the 'x', 'y', and 'z' values for each point $\endgroup$ – MarcClintDion Apr 14 '14 at 6:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.