平滑曲線生成是一個很實用的技術
很多時候,我們都需要通過繪制一些折線,然后讓計算機平滑的連接起來,或者是生成一些平滑的面
這里介紹利用一種貝塞爾曲線擬合的方法,先給出我們最終的效果

圖1 、折線擬合 圖2、多邊形擬合(封閉的折線)
繼續閱讀本文之前,你需要先掌握貝塞爾曲線的基本知識,這個網上資料很多,這里直接給出源代碼
//count為插入點數, outPoints為輸出點集合,長度為count + 2(含首尾) void BezierHelper::parseBezier(const Ogre::Vector2& start, const Ogre::Vector2& end, const Ogre::Vector2& control1, const Ogre::Vector2& control2, int count, std::vector<Ogre::Vector2>& outPoints) { if(count < 0) count = Bezier3DefaultPointNum; outPoints.push_back(start); for(int i = 1; i<=count; i++) { double st = (double) i/(count+1); double dt = (1-st); //二次項 double st2 = st * st; double dt2 = dt * dt; double t0 = dt * dt2; double t1 = dt2 * st * 3; double t2 = dt * st2 * 3; double t3 = st * st2; outPoints.push_back(start * t0 + control1 * t1 + control2 * t2 + end * t3); } outPoints.push_back(end); }
注:本文實現使用了Ogre的Vector2類,是一個表示二維坐標點的結構,
因為其同時重載了常用了+-*/等算數運算,代碼看起來會比較簡潔,本文最后會附上裁剪的Vector2源文件
算法原理:

圖3:當前點pt,當前點前后兩點p1、p2, pt點對應的前后控制點c1、c2
- 平滑
參考圖3,對於p1-pt-p2兩段線,算法生成p1~pt 和 pt~p2兩條貝塞爾曲線
兩條貝塞爾曲線在pt點對應的控制點分別為c1、c2
因為貝塞爾曲線與該點和其控制點的連線相切,所以保證c1、pt、c2三點共線就可以產生平滑的曲線
- 控制點自動計算
手工控制各控制點是很繁瑣的,如何自動計算控制點是本算法的核心,算法實現方式為:
- 取c1、pt、c2為角p1-pt-p2的角平分線的垂線
- 取c1-pt與c2-pt等長,為p1或p2在該垂線上的投影點,(參看圖3,當p1-pt長度大於pt-p2時,取p11點在垂線的投影點作為c1,p11到pt的距離與pt-p2等長)
- 對c1、c2做一定比例的縮放,實際取的控制點距pt的距離為投影點距離的0.2-0.6之間時都可以取得很好的平滑效果
實現:
1 //根據當前點pt,前一點p1, 后一點p2計算當前點對應的控制點control1 control2 2 void BezierHelper::getControlPoint(const Ogre::Vector2& pt, const Ogre::Vector2& p1, const Ogre::Vector2& p2, 3 Ogre::Vector2& control1, Ogre::Vector2& control2, double ratio) 4 { 5 double length1 = (p1 - pt).length(); 6 double length2 = (p2 - pt).length(); 7 8 double v = length2/(length1+ 0.000001); 9 10 Ogre::Vector2 delta; 11 if(v>1) 12 { 13 delta = p1 - (pt + ( p2 - pt) / (v + 0.000001)); 14 } 15 else 16 { 17 delta = pt + (p1-pt) * v - p2 ; 18 } 19 20 delta *= ratio; 21 control1 = pt + delta; 22 control2 = pt - delta; 23 }
ratio參數為調整系數,因為相對與總長,一般取0.1-0.3之間
- 折線生成
同樣方法可以計算p1,p2的前后控制點,這樣通過p1和p1的后控制點、pt和c1可以繪制曲線p1~pt,依次類推....
對於首尾節點,可以簡單的取其控制點為自身
實現:
void BezierHelper::parsePolyline(const std::vector<Ogre::Vector2>& points, int count, std::vector<Ogre::Vector2>& outPoints, double ratio) { std::vector<Ogre::Vector2>::size_type pointsSize = points.size(); if(pointsSize < 3 )//插值至少需要三點 { for(int i = 0; i<pointsSize; i++) { outPoints.push_back(points[i]); } } else { Ogre::Vector2 control1, control2; //頂點對應的前后控制點 getControlPoint(points[1], points[0], points[2], control1, control2, ratio); //首端 _parseBezier(points[0], points[1], points[0], control1, count, outPoints); for(int i = 2; i<pointsSize -1; i++) //根據中間各點生成與前一點連線 { Ogre::Vector2 lastControl = control2; getControlPoint(points[i], points[i-1], points[i+1], control1, control2, ratio); _parseBezier(points[i - 1], points[i], lastControl, control1, count, outPoints); } _parseBezier(points[pointsSize-2], points[pointsSize-1], control2, points[pointsSize-1], count, outPoints); outPoints.push_back(points[pointsSize-1]); } }
在運行這段代碼之前,我們需要修改前面的parseBezier函數為_parseBezier,區別僅在於不包括尾節點,方便折線、多邊形生成代碼的調用:
void BezierHelper::_parseBezier(const Ogre::Vector2& start, const Ogre::Vector2& end, const Ogre::Vector2& control1, const Ogre::Vector2& control2, int count, std::vector<Ogre::Vector2>& outPoints) { if(count < 0) count = Bezier3DefaultPointNum; outPoints.push_back(start); for(int i = 1; i<=count; i++) { double st = (double) i/(count+1); double dt = (1-st); //二次項 double st2 = st * st; double dt2 = dt * dt; double t0 = dt * dt2; double t1 = dt2 * st * 3; double t2 = dt * st2 * 3; double t3 = st * st2; outPoints.push_back(start * t0 + control1 * t1 + control2 * t2 + end * t3); } }
- 多邊形生成
與折線區別僅在於首點控制點需要通過尾節點生成,尾節點需要利過首節點生成
實現:
void BezierHelper::parsePolygon(const std::vector<Ogre::Vector2>& points, int count, std::vector<Ogre::Vector2>& outPoints, double ratio) { std::vector<Ogre::Vector2>::size_type pointsSize = points.size(); if(pointsSize < 3 )//插值至少需要三點 { for(int i = 0; i<pointsSize; i++) { outPoints.push_back(points[i]); } } else { Ogre::Vector2 control1, control2; //頂點對應的前后控制點 Ogre::Vector2 firstControl; Ogre::Vector2 lastControl; //首尾 getControlPoint(points[pointsSize-1], points[pointsSize-2], points[0], firstControl, lastControl, ratio); getControlPoint(points[0], points[pointsSize-1], points[1], control1, control2, ratio); _parseBezier(points[pointsSize-1], points[0], lastControl, control1, count, outPoints); for(int i = 1; i<pointsSize-1; i++) //根據中間各點,生成與前一點連線 { lastControl = control2; getControlPoint(points[i], points[i-1], points[i+1], control1, control2, ratio); _parseBezier(points[i-1], points[i], lastControl, control1, count, outPoints); } parseBezier(points[pointsSize-2], points[pointsSize-1], control2, firstControl, count, outPoints); //末端 outPoints.pop_back(); } }
附:Ogre::Vector2
#ifndef __Vector2_H__ #define __Vector2_H__ #include <assert.h> namespace Ogre { class Vector2 { public: double x, y; public: inline Vector2() { } inline Vector2(const double fX, const double fY ) : x( fX ), y( fY ) { } inline Vector2& operator = ( const Vector2& rkVector ) { x = rkVector.x; y = rkVector.y; return *this; } inline Vector2& operator = ( const double fScalar) { x = fScalar; y = fScalar; return *this; } inline bool operator == ( const Vector2& rkVector ) const { return ( x == rkVector.x && y == rkVector.y ); } inline bool operator != ( const Vector2& rkVector ) const { return ( x != rkVector.x || y != rkVector.y ); } // arithmetic operations inline Vector2 operator + ( const Vector2& rkVector ) const { return Vector2( x + rkVector.x, y + rkVector.y); } inline Vector2 operator - ( const Vector2& rkVector ) const { return Vector2( x - rkVector.x, y - rkVector.y); } inline Vector2 operator * ( const double fScalar ) const { return Vector2( x * fScalar, y * fScalar); } inline Vector2 operator * ( const Vector2& rhs) const { return Vector2( x * rhs.x, y * rhs.y); } inline Vector2 operator / ( const double fScalar ) const { assert( fScalar != 0.0 ); double fInv = 1.0f / fScalar; return Vector2( x * fInv, y * fInv); } inline Vector2 operator / ( const Vector2& rhs) const { return Vector2( x / rhs.x, y / rhs.y); } inline const Vector2& operator + () const { return *this; } inline Vector2 operator - () const { return Vector2(-x, -y); } // overloaded operators to help Vector2 inline friend Vector2 operator * ( const double fScalar, const Vector2& rkVector ) { return Vector2( fScalar * rkVector.x, fScalar * rkVector.y); } inline friend Vector2 operator / ( const double fScalar, const Vector2& rkVector ) { return Vector2( fScalar / rkVector.x, fScalar / rkVector.y); } inline friend Vector2 operator + (const Vector2& lhs, const double rhs) { return Vector2( lhs.x + rhs, lhs.y + rhs); } inline friend Vector2 operator + (const double lhs, const Vector2& rhs) { return Vector2( lhs + rhs.x, lhs + rhs.y); } inline friend Vector2 operator - (const Vector2& lhs, const double rhs) { return Vector2( lhs.x - rhs, lhs.y - rhs); } inline friend Vector2 operator - (const double lhs, const Vector2& rhs) { return Vector2( lhs - rhs.x, lhs - rhs.y); } // arithmetic updates inline Vector2& operator += ( const Vector2& rkVector ) { x += rkVector.x; y += rkVector.y; return *this; } inline Vector2& operator += ( const double fScaler ) { x += fScaler; y += fScaler; return *this; } inline Vector2& operator -= ( const Vector2& rkVector ) { x -= rkVector.x; y -= rkVector.y; return *this; } inline Vector2& operator -= ( const double fScaler ) { x -= fScaler; y -= fScaler; return *this; } inline Vector2& operator *= ( const double fScalar ) { x *= fScalar; y *= fScalar; return *this; } inline Vector2& operator *= ( const Vector2& rkVector ) { x *= rkVector.x; y *= rkVector.y; return *this; } inline Vector2& operator /= ( const double fScalar ) { assert( fScalar != 0.0 ); double fInv = 1.0f / fScalar; x *= fInv; y *= fInv; return *this; } inline Vector2& operator /= ( const Vector2& rkVector ) { x /= rkVector.x; y /= rkVector.y; return *this; } /** Returns the length (magnitude) of the vector. @warning This operation requires a square root and is expensive in terms of CPU operations. If you don't need to know the exact length (e.g. for just comparing lengths) use squaredLength() instead. */ inline double length () const { return sqrt( x * x + y * y ); } /** Returns the square of the length(magnitude) of the vector. @remarks This method is for efficiency - calculating the actual length of a vector requires a square root, which is expensive in terms of the operations required. This method returns the square of the length of the vector, i.e. the same as the length but before the square root is taken. Use this if you want to find the longest / shortest vector without incurring the square root. */ inline double squaredLength () const { return x * x + y * y; } /** Returns the distance to another vector. @warning This operation requires a square root and is expensive in terms of CPU operations. If you don't need to know the exact distance (e.g. for just comparing distances) use squaredDistance() instead. */ inline double distance(const Vector2& rhs) const { return (*this - rhs).length(); } /** Returns the square of the distance to another vector. @remarks This method is for efficiency - calculating the actual distance to another vector requires a square root, which is expensive in terms of the operations required. This method returns the square of the distance to another vector, i.e. the same as the distance but before the square root is taken. Use this if you want to find the longest / shortest distance without incurring the square root. */ inline double squaredDistance(const Vector2& rhs) const { return (*this - rhs).squaredLength(); } /** Calculates the dot (scalar) product of this vector with another. @remarks The dot product can be used to calculate the angle between 2 vectors. If both are unit vectors, the dot product is the cosine of the angle; otherwise the dot product must be divided by the product of the lengths of both vectors to get the cosine of the angle. This result can further be used to calculate the distance of a point from a plane. @param vec Vector with which to calculate the dot product (together with this one). @return A float representing the dot product value. */ inline double dotProduct(const Vector2& vec) const { return x * vec.x + y * vec.y; } /** Normalises the vector. @remarks This method normalises the vector such that it's length / magnitude is 1. The result is called a unit vector. @note This function will not crash for zero-sized vectors, but there will be no changes made to their components. @return The previous length of the vector. */ inline double normalise() { double fLength = sqrt( x * x + y * y); // Will also work for zero-sized vectors, but will change nothing // We're not using epsilons because we don't need to. // Read http://www.ogre3d.org/forums/viewtopic.php?f=4&t=61259 if ( fLength > double(0.0f) ) { double fInvLength = 1.0f / fLength; x *= fInvLength; y *= fInvLength; } return fLength; } /** Returns a vector at a point half way between this and the passed in vector. */ inline Vector2 midPoint( const Vector2& vec ) const { return Vector2( ( x + vec.x ) * 0.5f, ( y + vec.y ) * 0.5f ); } /** Returns true if the vector's scalar components are all greater that the ones of the vector it is compared against. */ inline bool operator < ( const Vector2& rhs ) const { if( x < rhs.x && y < rhs.y ) return true; return false; } /** Returns true if the vector's scalar components are all smaller that the ones of the vector it is compared against. */ inline bool operator > ( const Vector2& rhs ) const { if( x > rhs.x && y > rhs.y ) return true; return false; } /** Sets this vector's components to the minimum of its own and the ones of the passed in vector. @remarks 'Minimum' in this case means the combination of the lowest value of x, y and z from both vectors. Lowest is taken just numerically, not magnitude, so -1 < 0. */ inline void makeFloor( const Vector2& cmp ) { if( cmp.x < x ) x = cmp.x; if( cmp.y < y ) y = cmp.y; } /** Sets this vector's components to the maximum of its own and the ones of the passed in vector. @remarks 'Maximum' in this case means the combination of the highest value of x, y and z from both vectors. Highest is taken just numerically, not magnitude, so 1 > -3. */ inline void makeCeil( const Vector2& cmp ) { if( cmp.x > x ) x = cmp.x; if( cmp.y > y ) y = cmp.y; } /** Generates a vector perpendicular to this vector (eg an 'up' vector). @remarks This method will return a vector which is perpendicular to this vector. There are an infinite number of possibilities but this method will guarantee to generate one of them. If you need more control you should use the Quaternion class. */ inline Vector2 perpendicular(void) const { return Vector2 (-y, x); } /** Calculates the 2 dimensional cross-product of 2 vectors, which results in a single floating point value which is 2 times the area of the triangle. */ inline double crossProduct( const Vector2& rkVector ) const { return x * rkVector.y - y * rkVector.x; } /** Returns true if this vector is zero length. */ inline bool isZeroLength(void) const { double sqlen = (x * x) + (y * y); return (sqlen < (1e-06 * 1e-06)); } /** As normalise, except that this vector is unaffected and the normalised vector is returned as a copy. */ inline Vector2 normalisedCopy(void) const { Vector2 ret = *this; ret.normalise(); return ret; } /** Calculates a reflection vector to the plane with the given normal . @remarks NB assumes 'this' is pointing AWAY FROM the plane, invert if it is not. */ inline Vector2 reflect(const Vector2& normal) const { return Vector2( *this - ( 2 * this->dotProduct(normal) * normal ) ); } }; } #endif
