2

我最近为 Maya 编写了一个脚本,该脚本导出了一个包含 Bezier 路径(一系列 xy 点和 xy 控制点)数据的文件。

此贝塞尔路径旨在表示我的角色将在应用程序内以恒定速度行驶的 3D“轨道”或路径。

我了解如何构建 UIBezierCurve,但我似乎无法找到任何可靠的信息,如果它可能/如何获得曲线上一个点的 x / y 位置,给定沿曲线行进的距离。

我在苹果上找到了这个列表:

http://lists.apple.com/archives/cocoa-dev/2002/Feb/msg01806.html

但我不太明白该函数返回什么以及如何使用它来完成我的目标。

任何帮助/建议将不胜感激,

谢谢,-亚当·艾斯菲尔德

4

2 回答 2

1

好吧,这将是一个很长的答案。这是我所做的:

  1. 我编写了一个 MEL 脚本,允许您在 Maya 中绘制贝塞尔曲线,然后 - 选择该曲线 - 运行我的脚本,该脚本将通过曲线分析曲线的每个贝塞尔部分,计算每个部分的长度和曲线的位置点/控制点。一旦计算了所有这些数据,它会将所有内容导出到一个 .bezier 文件,该文件的结构如下:

    第 1 行:整个贝塞尔路径中包含的单个贝塞尔曲线的数量 第 2 行:第一条贝塞尔曲线的长度 ... X 行:最后一条贝塞尔曲线的长度

    X 第一个曲线点的第一个控制点的位置 Y 第一个曲线点的第一个控制点的位置 Z 第一个曲线点的第一个控制点的位置

    X 第一个曲线点的位置 Y 第一个曲线点的位置 Z 第一个曲线点的位置

    X 第一个曲线点的第二个控制点的位置 Y 第一个曲线点的第二个控制点的位置 Z 第一个曲线点的第二个控制点的位置

    ...

    X 最后一个曲线点的第一个控制点的位置 Y 最后一个曲线点的第一个控制点的位置 Z 最后一个曲线点的第一个控制点的位置

    X 最后一个曲线点的位置 Y 最后一个曲线点的位置 Z 最后一个曲线点的位置

    X 最后一个曲线点的第二个控制点的位置 Y 最后一个曲线点的第二个控制点的位置 Z 最后一个曲线点的第二个控制点的位置

因此,要使这组类正常工作,您需要一个类似结构的文件。

以下是我为处理 .bezier 文件而编写的三个类:

AEBezier路径:

.h 文件:

#import <Foundation/Foundation.h>
#import "AEBezierVertex.h"
#import "AEBezierLine.h"

@interface AEBezierPath : NSObject
{
    NSMutableArray *vertices;
    NSMutableArray *lines;
    UIBezierPath *path;
}

@property (strong) NSMutableArray *vertices;
@property (strong) NSMutableArray *lines;
@property (strong) UIBezierPath *path;

-(id) initFromFile: (NSString*) file;
-(CGPoint) positionFromDistance: (float) fromDistance;

@end

.m 文件:

#import "AEBezierPath.h"

CGFloat bezierInterpolation(CGFloat t, CGFloat a, CGFloat b, CGFloat c, CGFloat d) {
    // see also below for another way to do this, that follows the 'coefficients'
    // idea, and is a little clearer
    CGFloat t2 = t * t;
    CGFloat t3 = t2 * t;
    return a + (-a * 3 + t * (3 * a - a * t)) * t
    + (3 * b + t * (-6 * b + b * 3 * t)) * t
    + (c * 3 - c * 3 * t) * t2
    + d * t3;
}

@implementation AEBezierPath
@synthesize vertices;
@synthesize lines;
@synthesize path;

-(id) initFromFile: (NSString*) file
{
    self = [super init];
    if (self) {

        //Init file objects for reading
        NSError *fileError;
        NSStringEncoding *encoding;

        vertices = [[NSMutableArray alloc] init];
        lines = [[NSMutableArray alloc] init];
        path = [[UIBezierPath alloc] init]; 

        //Load the specified file's contents into an NSString
        NSString *fileData = [[NSString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"testcurve" ofType:@"bezier"] usedEncoding:&encoding error:&fileError];
        NSScanner *scanner = [[NSScanner alloc] initWithString:fileData];

        if(fileData == nil)
        {
            NSLog(@"Error reading bezier path file");
        }
        else
        {
            float x;
            float y;
            float cx;
            float cy;
            float cx2;
            float cy2;
            float temp;

            CGPoint readPoint;
            CGPoint readControlIn;
            CGPoint readControlOut;

            int curRead = 0;
            int totalSegments = 0;
            float length;

            [scanner scanInt:&totalSegments];

            for (int s = 0; s < totalSegments; s++) {
                [scanner scanFloat:&length];
                AEBezierLine *newLine = [[AEBezierLine alloc] initWithLength:length];

                [lines addObject:newLine];
            }

            AEBezierVertex *vertex;

            while ([scanner isAtEnd] == 0) {

                if (curRead == 0) {
                    [scanner scanFloat:&x];
                    [scanner scanFloat:&temp];
                    [scanner scanFloat:&y];


                    [scanner scanFloat:&cx2];
                    [scanner scanFloat:&temp];
                    [scanner scanFloat:&cy2];  

                    cx = x;
                    cy = y;
                }

                else{

                    [scanner scanFloat:&cx];
                    [scanner scanFloat:&temp];
                    [scanner scanFloat:&cy];

                    [scanner scanFloat:&x];
                    [scanner scanFloat:&temp];
                    [scanner scanFloat:&y];

                    if ([scanner isAtEnd] == 0) {
                        [scanner scanFloat:&cx2];
                        [scanner scanFloat:&temp];
                        [scanner scanFloat:&cy2];
                    }else
                    {
                        cx = x;
                        cy = y;
                    }
                }

                readPoint = CGPointMake(x, y);
                readControlIn = CGPointMake(cx, cy);
                readControlOut = CGPointMake(cx2, cy2);

                vertex = [[AEBezierVertex alloc] initWithControl:readPoint In:readControlIn Out:readControlOut];

                [vertices addObject:vertex];

                curRead ++;

            }

            for (int c = 0; c < [vertices count]-1; c++) {

                //Init CGPoints for single bezier curve segment
                CGPoint p1, p2, p3, p4;

                //Store starting bezier point and control point
                AEBezierVertex *b1 = [vertices objectAtIndex:c];
                p1 = b1.control;
                p2 = b1.controlOut;    

                //Store ending bezier point and control point
                AEBezierVertex *b2 = [vertices objectAtIndex:c+1];
                p3 = b2.controlIn;
                p4 = b2.control;

                if (c == 0) {
                    [path moveToPoint:p1];
                }
                else
                {
                    [path addCurveToPoint:p4 controlPoint1:p2 controlPoint2:p3];
                }
            }
        }
    }
    return self;
}

-(CGPoint) positionFromDistance: (float) fromDistance
{
    CGPoint position;


    AEBezierLine *line;
    float runningLength;
    int seg = 0;

    for (int c = 0; c < [lines count]; c++) {
        seg = c;
        line = [lines objectAtIndex:c];
        runningLength += line.length;
        if (runningLength > fromDistance) {
            break;
        }
    }

    CGPoint p1, p2, p3, p4;

    AEBezierVertex *vert1 = [vertices objectAtIndex:seg];
    p1 = vert1.control;
    p2 = vert1.controlOut;    

    //Store ending bezier point and control point
    AEBezierVertex *vert2 = [vertices objectAtIndex:seg+1];
    p3 = vert2.controlIn;
    p4 = vert2.control;

    float travelDist;
    travelDist = fromDistance;

    travelDist = runningLength - travelDist;
    travelDist = line.length - travelDist;

    float t = travelDist / line.length;

    //Create a new point to represent this position
    position = CGPointMake(bezierInterpolation(t, p1.x, p2.x, p3.x, p4.x),
                                 bezierInterpolation(t, p1.y, p2.y, p3.y, p4.y));    

    return position;
}

@end

AEBezier顶点:

.h 文件:

#import <Foundation/Foundation.h>

@interface AEBezierVertex : NSObject
{
    CGPoint controlIn;
    CGPoint controlOut;
    CGPoint control;
}
@property CGPoint controlIn;
@property CGPoint controlOut;
@property CGPoint control;

-(id) initWithControl: (CGPoint) setControl In: (CGPoint) setIn Out: (CGPoint) setOut;

@end

.m 文件:

#import "AEBezierVertex.h"

@implementation AEBezierVertex
@synthesize controlIn;
@synthesize controlOut;
@synthesize control;

-(id) initWithControl: (CGPoint) setControl In: (CGPoint) setIn Out: (CGPoint) setOut
{
    self = [super init];
    if (self) {
        //Init
        control = setControl;
        controlIn = setIn;
        controlOut = setOut;
    }
    return self;
}

@end

AEBezier线:

.h 文件:

#import <Foundation/Foundation.h>

@interface AEBezierLine : NSObject
{
    float length;
}
@property float length;

-(id) initWithLength: (float) setLength;

@end

.m 文件:

#import "AEBezierLine.h"

@implementation AEBezierLine
@synthesize length;

-(id) initWithLength: (float) setLength
{
    self = [super init];
    if (self) {
        //Init
        length = setLength;
    }
    return self;
}

@end

这个怎么运作:

  1. 确保您已经创建了一个适合我上面显示的结构的 .bezier 文件,并将其放在您的应用程序包中。

  2. 通过以下方式实例化一个新的 AEBezierPath 实例:

    -(id) initFromFile: (NSString*) 文件;

这将从名为 *file 的 .bezier 文件中读取所有数据,并从中构造一个 UIBezierPath,并将必要的长度信息存储到 AEBezierPath 中。

  1. 查询 AEBezierPath 以获取 CGPoint 形式的 x/y 位置,方法是向其发送从路径起点行进的距离值,使用以下方法:

    -(CGPoint) positionFromDistance: (float) fromDistance;

此方法将首先通过使用先前从 .bezier 文件中检索到的每个贝塞尔线段的长度来确定该距离所在的贝塞尔线段。在此之后,该方法将使用此 SO Question 的前几篇文章中提到的 bezierInterpolation 函数来计算此距离处贝塞尔路径上的 x/y 位置,并将其作为 CGPoint 返回。

它并不完美,在长贝塞尔曲线和短弯角上行驶的距离仍然存在一些明显的差异,但与完全不使用这个系统而是依靠百分比值沿着贝塞尔曲线行驶相比,它肯定远没有那么明显.

我知道代码当然可以优化,这只是让一切正常工作的第一次运行,但我认为它足以作为现在的答案发布。

  • 亚当·艾斯菲尔德
于 2012-01-18T22:10:04.850 回答
0

您引用的链接暗示的是贝塞尔曲线的每一段都描绘出一条路径(x(t),y(t)),其中t从0变为1。

我不熟悉UIBezierCurve,但我敢打赌你可以从中得到一个NSBezierPath,然后你可以从那里手动遍历这些段。每个段要么是 moveTo、lineTo、curveTo 或 close(相当于最后一个 moveTo 位置的 lineTo)。唯一重要的路径类型是curveTo,您可以在此处阅读更多信息:

http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves

如果您只想为沿曲线移动的动画赋予每个固定的时间,那将很简单;您可以遍历这些段并在每个段中,从 0 到 1 逐渐运行 t 并插入等式。

棘手的部分将以恒定的速度移动。为此,您需要实际测量每个段的长度并将该长度拆分为每个帧的部分。您可以在这个问题中阅读更多相关信息:

贝塞尔曲线上的等距点

我有一段时间没有使用 Cocoa 了,但是我在这里有一些Java 代码,你可以很容易地移植(这只是数学,在任何语言中都是一样的):

示例程序的输出

于 2012-01-16T23:34:50.910 回答