Кривые Безье.

Содержание статьи:

Введение.
Алгоритмическая база.
Реализация.

Введение.

к началу статьи
Приветствую всех! Сейчас 2:17 ночи, через 3 часа мне нужно будет сорваться с места и бежать на работу, что бы хотя бы в этот раз сдать релиз вовремя. Нашей службе это никогда прежде не удавалось... Но в этот раз, я уверен, нам повезет! Не только этот день, а вообще последние несколько недель выдались чересчур жаркими даже для меня: абсолютно нереальный государственный экзамен за все 4 года учебы, шлифовка бакалавра с последующей блестящей защитой, опять-таки релиз.... Все свободное время занимал сон (пять или шесть часов, не больше), но сегодня я решил от него отказаться ради того, что бы продемонстрировать Вам как работать с кубическими кривыми Безье. Если по каким-то причинам Вы о них никогда не слыхали, не отчаивайтесь – я постарался сделать статью максимально доступной. За сим считаю лирическое отступление законченным. Можно кодить?

Алгоритмическая база.

к началу статьи
Кривая Безье задается несколькими точками в N-мерном пространстве (классический вариант, описываемый в литературе – 4 точки, но мы, для удобства, помимо этого случая будем использовать вырожденные случаи из 3-х точек, а так же пользоваться введением дополнительных вершин), но нам так много не нужно, поэтому ограничимся тремя измерениями (хотя все приведенные алгоритмы будут справедливы и для векторов другой мерности).

Для начала рассмотрим самый простой случай (его иллюстрация этого случая приведена на рис. 1) – когда у нас 4 вершины.

Здесь p0 и p3 – начальная и конечная точки соответственно, а p1 и p2 – промежуточные точки. Разности же вершин p1-p0 и p2-p3 определяют крутизну кривой в этих точках. Сама же кривая задается следующим уравнением:


C(s) = p0 * ( 1 – s ) ^ 3 + 3 * p1 * s * ( 1 – s )^2 + 3 * p2 * s ^ 2 * ( 1 – s ) + p3 * s ^ 3
Несложно показать, что

C( 0 ) = p0 
C( 1 ) = p3 

C’( 0 ) = 3 * ( p1 - p0 ) 
C’( 1 ) = 3 * ( p3 – p2 )
Собственно как я и говорил – кривая проходит через конечные точки + имеется информация о крутизне кривой.

Но можно построить кривую Безье и по трем точкам. В этом случае p1 и p2 будут одинаковые. Правда в этом случае мы лишимся возможности перекручивать нашу кривую, плюс сама кривая будет не такая гладкая, как если бы она строилась по четырём точкам (рис. 2).

Наверняка с кривыми из 4 вершин Вы быстро наиграетесь и захотите задавать более длинные и изощренные кривые с петлями, изломами… Из 4 точек их уже не составить, поэтому придется стыковать их. Но как это осуществить? Если мя просто сделаем так, что бы последняя вершина первой кривой совпадала с первой вершиной второй кривой, то мы получим излом. Это связано с тем, что производная меняется скачком – при подходе к концу кривой она у нас выгнута в одну сторону, а при проходе точки стыка она у нас мгновенно выгибается в другую сторону. Что бы этого не произошло, нам придется контролировать производную в точках стыка кусков кривых Безье. Вручную это делать как-то тоскливо, а автоматически не получиться, поэтому придется исхитриться. Собственно ничего сложного мы делать не будем – все детали Вы можете увидеть на рис. 3.

Реализация.

к началу статьи
По доброй (я надеюсь) традиции сначала рассмотрим класс:

#define			BC_LOOPED					1
#define			BC_TRI_VERTEX_CURVE				2
#define			BC_QUAD_VERTEX_CURVE				4
#define			BC_ABSOLUTE_LENGTH				8
#define			BC_RELATIVE_LENGTH				16

class		cBesierCurve{
	cArray<cVector4>	*Vertexes;
	cArray<float>		*SubCurveLengths;
	int			MODE;
	float			GetLength( cVector4 , cVector4 , cVector4 , cVector4 );
public:
	float			Length;

	cBesierCurve( void ){Length = -1;Vertexes=NULL;SubCurveLengths=NULL;}

	cVector4		operator()( float );
	cVector4		operator()( float , cVector4 , cVector4 , cVector4 , cVector4 );
	void			AddVertex( cVector4 );
	void			SetMode( int _m ){MODE = _m;}
	float			GetLength( void );
	void			Render( cD3DDevice & , float );
	void			RenderCurveStructure( cD3DDevice & );
};
Хочу обратить Ваше внимание на флаги в самом начале листинга, их значение следующее:

BC_LOOPED – из множества точек, хранящихся в массиве Vertexes будет создана составная зацикленная кривая Безье.
BC_TRI_VERTEX_CURVE – куски кривых Безье будут строится по 3 точкам (рис. 2)
BC_QUAD_VERTEX_CURVE – куски кривых Безье будут строится по 4точкам (рис. 1)
BC_ABSOLUTE_LENGTH и BC_RELATIVE_LENGTH – длина кривой будет измеряться в абсолютных или относительных величинах соответственно.

Создание нашей кривой начинается с определения точек, из которых она состоит:


void				cBesierCurve::AddVertex( cVector4 v ){
	if( !Vertexes ){
		Vertexes = new cArray<cVector4>;
	}
	Vertexes->AddEnd( v );
}
После чего устанавливаем тип кривой – SetMode( int _m ).

Затем осуществляем препроцессинг нашей кривой (рассмотрим здесь только часть функции) –


//минимальные проверки
	if( !Vertexes ){
		MessageBox( 0 , "В кривой нет вершин." , "Ошибка" , 0 );
		exit( 0 );
	}

	if( !SubCurveLengths ){
		SubCurveLengths = new cArray<float>;
	}
//смотрим что за кривая
	if( ( MODE & BC_QUAD_VERTEX_CURVE ) && ( MODE & BC_LOOPED ) ){
Здесь мы проверяем что в зацикленной кривой, строящейся по 4 точкам, достаточно точек. Для данной кривой должно выполняться следующее условие - ( Vertexes->Cursor - 2 ) % 4. Для кривых другого типа это условие проще.

		if( ( Vertexes->Cursor - 2 ) % 4 ){
			MessageBox( 0 , "В кривой такого типа недостаточно вершин" 
				, "Ошибка" , 0 );
			return( 0 );
		}
Наша кривая состоит из кусков кривых Безье. Для того что бы эффективно пользоваться этим классом вычислим длины этих частей заранее и сохраним их в массиве SubCurveLengths. Кстати длина куска Безье вычисляется функцией GetLength(cVector4 , cVector4 , cVector4 , cVector4 ).

			Length = 0;
			SubCurveLengths->SetLength( Vertexes->Cursor / 2 );

			for( int i( 0 ) ; i < Vertexes->Cursor / 2 ; i++ ){
				int i0( 2 * i + 0 - Vertexes->Cursor * ( ( 2 * i + 0 ) / 
						Vertexes->Cursor ) );
				int i1( 2 * i + 1 - Vertexes->Cursor * ( ( 2 * i + 1 ) / 
						Vertexes->Cursor ) );
				int i2( 2 * i + 2 - Vertexes->Cursor * ( ( 2 * i + 2 ) / 
						Vertexes->Cursor ) );
				int i3( 2 * i + 3 - Vertexes->Cursor * ( ( 2 * i + 3 ) / 
						Vertexes->Cursor ) );

				(*SubCurveLengths)[ i ] = GetLength(0.5 * ( (*Vertexes)[ i0 ] + 
						(*Vertexes)[ i1 ] ) , (*Vertexes)[ i1 ] , 
						(*Vertexes)[ i2 ] , 0.5 * ( (*Vertexes)[ i3 ] + 
						(*Vertexes)[ i2 ] ) );

				Length += (*SubCurveLengths)[ i ];
			}
			if( MODE & BC_RELATIVE_LENGTH ){
				for( int i( 0 ) ; i < SubCurveLengths->Cursor ; i++ ){
					(*SubCurveLengths)[ i ] /= Length;
				}
				Length = 1;
			}
			return( Length );
		}
	}
Смысл же флагов BC_RELATIVE_LENGTH и BC_ABSOLUTE_LENGTH заключается в том, что в первом случае длина кривой будет из промежутка от 0 до 1, а во втором той длины, какая есть. На последок скажу, что функция GetLength(cVector4 , cVector4 , cVector4 , cVector4 ) длину кривой приблизительно, хотя и достаточно точно. Все необходимы приготовления сделаны и можно наконец приступать к самому главному – параметризации нашей кривой:

cVector4			cBesierCurve::operator()( float t ){
	float		tmp( NULL );
	int			i( 0 );
	float			s;
	if( ( MODE & BC_LOOPED ) ){
		for( ; i < SubCurveLengths->Cursor && tmp + 
				(*SubCurveLengths)[ i ] < t ; i++ ){
			tmp += (*SubCurveLengths)[ i ];
		}
		if( i == SubCurveLengths->Cursor )
			s = ( ( t - tmp ) / (*SubCurveLengths)[ i - 1 ] );
		else
			s = ( ( t - tmp ) / (*SubCurveLengths)[ i ] );
	}
	else{
		for( ; i < SubCurveLengths->Cursor - 1 
				&& tmp + (*SubCurveLengths)[ i ] < t ; i++ ){
			tmp += (*SubCurveLengths)[ i ];
		}
		if( i == SubCurveLengths->Cursor )
			s = ( ( t - tmp ) / (*SubCurveLengths)[ i - 1 ] );
		else
			s = ( ( t - tmp ) / (*SubCurveLengths)[ i ] );
	}

	if( MODE & BC_TRI_VERTEX_CURVE ){
		int i0( i + 0 - Vertexes->Cursor * ( ( i + 0 ) / Vertexes->Cursor ) );
		int i1( i + 1 - Vertexes->Cursor * ( ( i + 1 ) / Vertexes->Cursor ) );
		int i2( i + 2 - Vertexes->Cursor * ( ( i + 2 ) / Vertexes->Cursor ) );

		return( (*this)( s , 0.5 * ( (*Vertexes)[ i0 ] + (*Vertexes)[ i1 ] ) , 
					(*Vertexes)[ i1 ] , (*Vertexes)[ i1 ] , 
					0.5 * ( (*Vertexes)[ i2 ] + (*Vertexes)[ i1 ] )) );
	}
	if( MODE & BC_QUAD_VERTEX_CURVE ){
		int i0( 2 * i + 0 - Vertexes->Cursor * ( ( 2 * i + 0 ) / Vertexes->Cursor ) );
		int i1( 2 * i + 1 - Vertexes->Cursor * ( ( 2 * i + 1 ) / Vertexes->Cursor ) );
		int i2( 2 * i + 2 - Vertexes->Cursor * ( ( 2 * i + 2 ) / Vertexes->Cursor ) );
		int i3( 2 * i + 3 - Vertexes->Cursor * ( ( 2 * i + 3 ) / Vertexes->Cursor ) );

		return( (*this)( s , 0.5 * ( (*Vertexes)[ i0 ] + (*Vertexes)[ i1 ] ) , 
					(*Vertexes)[ i1 ] , (*Vertexes)[ i2 ] , 
					 0.5 * ( (*Vertexes)[ i2 ] + (*Vertexes)[ i3 ] )) );
	}
	MessageBox( 0 , "Неопознанный тип кривой (operator())." , "Ошибка" , 0 );
	return( NULL );
}
В этой функции мы сначала находим курсор i – первой вершины (p0) в куске Безье, а затем с помощью функции

cVector4			cBesierCurve::operator()( float t , cVector4 p0 , 
					cVector4 p1 , cVector4 p2 , cVector4 p3 ){
	return( ( 1 - t ) * ( 1 - t ) * ( 1 - t ) * p0 + 
			3 * t * ( 1 - t ) * ( 1 - t ) * p1 + 
			3 * t * t * ( 1 - t ) * p2 +
			t * t * t * p3 );
}
находим точку на кривой.

Вот пожалуй и все на сегодня. До встречи!

к началу статьи


Исходные коды.
© 2004-2005 Savardge.ru


Hosted by uCoz