计算机图形学实验——二次Bezier曲线 & 双三次Bezier曲面绘制 (dialog-based MFC project) (附源码)
使用VS2022基于对话框的MFC项目模板,绘制二次贝塞尔曲线和双三次贝塞尔曲面
Bezier曲线曲面绘制
源码:Yushan-Ji/ComputerGraphics: ECNU2023秋 计算机图形学课程实验代码 (github.com)
ps.若github.com国内网络无法访问,可使用镜像Yushan-Ji/ComputerGraphics: ECNU2023秋 计算机图形学课程实验代码 (githubfast.com)
文章目录
实验内容
- 绘制低次Bezier、B样条或NURBS曲线
- 绘制双三次Bezier或B样条曲面
实验思路
二次Bezier曲线
二次贝塞尔曲线是由三个控制点定义的曲线,通常表示为 P 0 P_0 P0、 P 1 P_1 P1和 P 2 P_2 P2。曲线的公式如下:
B ( t ) = ( 1 − t ) 2 ⋅ P 0 + 2 ⋅ ( 1 − t ) ⋅ t ⋅ P 1 + t 2 ⋅ P 2 ( t ∈ [ 0 , 1 ] ) B(t) = (1 - t)^2 \cdot P_0 + 2 \cdot (1 - t) \cdot t \cdot P_1 + t^2 \cdot P_2 \quad (t\in [0,1]) B(t)=(1−t)2⋅P0+2⋅(1−t)⋅t⋅P1+t2⋅P2(t∈[0,1])
其中, P 0 P_0 P0是起始点, P 1 P_1 P1是控制点, P 2 P_2 P2是终点。
我们使用下面的公式计算曲线上的点:
{ x ( t ) = ( 1 − t ) 2 ⋅ x 0 + 2 ⋅ ( 1 − t ) ⋅ t ⋅ x 1 + t 2 ⋅ x 2 y ( t ) = ( 1 − t ) 2 ⋅ y 0 + 2 ⋅ ( 1 − t ) ⋅ t ⋅ y 1 + t 2 ⋅ y 2 \begin{cases} x(t) = (1 - t)^2 \cdot x_0 + 2 \cdot (1 - t) \cdot t \cdot x_1 + t^2 \cdot x_2 \\ y(t) = (1 - t)^2 \cdot y_0 + 2 \cdot (1 - t) \cdot t \cdot y_1 + t^2 \cdot y_2 \end{cases} {x(t)=(1−t)2⋅x0+2⋅(1−t)⋅t⋅x1+t2⋅x2y(t)=(1−t)2⋅y0+2⋅(1−t)⋅t⋅y1+t2⋅y2
其中, ( x 0 , y 0 ) (x_0, y_0) (x0,y0)是起始点坐标,$(x_1, y_1) 是控制点坐标, 是控制点坐标, 是控制点坐标,(x_2, y_2) 是终点坐标;通过给定一系列离散的 是终点坐标;通过给定一系列离散的 是终点坐标;通过给定一系列离散的t$值,实现绘制平滑的Bezier曲线(t越小曲线越平滑)。
双三次Bezier曲面
双三次贝塞尔曲面是由16个控制点定义的曲面,通常表示为4x4矩阵。
曲面的公式如下:
B ( u , v ) = ∑ i = 0 3 ∑ j = 0 3 P i j ⋅ B i 3 ( u ) ⋅ B j 3 ( v ) ( u , v ∈ [ 0 , 1 ] ) B(u, v) = \sum_{i=0}^3 \sum_{j=0}^3 P_{ij} \cdot B_i^3(u) \cdot B_j^3(v) \quad (u,v\in [0,1]) B(u,v)=i=0∑3j=0∑3Pij⋅Bi3(u)⋅Bj3(v)(u,v∈[0,1])
其中, P i j P_{ij} Pij是曲面上的控制点; B i 3 ( u ) B_i^3(u) Bi3(u)和 B j 3 ( v ) B_j^3(v) Bj3(v)是三次贝塞尔基函数,定义如下:
B i 3 ( u ) = C 3 i ⋅ u i ⋅ ( 1 − u ) 3 − i B j 3 ( v ) = C 3 j ⋅ v j ⋅ ( 1 − v ) 3 − j B_i^3(u) = C_3^i \cdot u^i \cdot (1 - u)^{3-i}\\ B_j^3(v) = C_3^j \cdot v^j \cdot (1 - v)^{3-j} Bi3(u)=C3i⋅ui⋅(1−u)3−iBj3(v)=C3j⋅vj⋅(1−v)3−j
计算曲面上的点的公式如下:
x ( u , v ) = ∑ i = 0 3 ∑ j = 0 3 x i j ⋅ B i 3 ( u ) ⋅ B j 3 ( v ) y ( u , v ) = ∑ i = 0 3 ∑ j = 0 3 y i j ⋅ B i 3 ( u ) ⋅ B j 3 ( v ) x(u, v) = \sum_{i=0}^3 \sum_{j=0}^3 x_{ij} \cdot B_i^3(u) \cdot B_j^3(v)\\ y(u, v) = \sum_{i=0}^3 \sum_{j=0}^3 y_{ij} \cdot B_i^3(u) \cdot B_j^3(v) x(u,v)=i=0∑3j=0∑3xij⋅Bi3(u)⋅Bj3(v)y(u,v)=i=0∑3j=0∑3yij⋅Bi3(u)⋅Bj3(v)
通过给定一系列离散的 u , v u,v u,v值,实现绘制平滑的Bezier曲面( u , v u,v u,v越小,面片就越小,曲面越平滑)。
核心代码
对话框
在resource.h
文件中新增下列组件:
#define IDD_BEZIERCURVE_DIALOG 129 // 主对话框
#define IDC_STATIC_CANVAS 1000 // 绘制曲线的绘图框
#define IDC_STATIC_CANVAS_SURFACE 1001 // 绘制曲面的绘图框
#define IDC_BUTTON1 1002 // 绘制曲面的按钮
对话框设计如下:

头文件
// BezierCurve.h : header file
#pragma once
#include <vector>
// BezierCurve dialog
class BezierCurve : public CDialogEx
{
// Construction
public:
BezierCurve(CWnd* pParent = nullptr); // standard constructor
// Dialog Data
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_BEZIER_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg void OnLButtonDown(UINT nFlags, CPoint point); // 左键点击
afx_msg void OnRButtonDown(UINT nFlags, CPoint point); // 右键点击
DECLARE_MESSAGE_MAP()
public:
// Bezier曲线
std::vector<CPoint> points;
bool IsCompleted;
CPoint BezierCurvePoint(CPoint p0, CPoint p1, CPoint p2, double t);
void DrawBezierCurve();
// Bezier曲面
CPoint controlPoints[4][4]; // 16个控制点
CPoint surfacePoints[50][50]; // 2500个曲面点
void GenerateControlPoints(); // 随机生成控制点
int Combination(int n, int k); // 计算组合数
CPoint BezierSurfacePoint(float u, float v); // 计算每一个曲面上的点
void DrawBezierSurface(); //绘制曲面
};
头文件中类BezierCurve
的构造函数如下:
BezierCurve::BezierCurve(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_BEZIERCURVE_DIALOG, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
IsCompleted = false;
points.clear();
}
消息函数
BEGIN_MESSAGE_MAP(BezierCurve, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_LBUTTONDOWN()
ON_WM_RBUTTONDOWN()
ON_BN_CLICKED(IDC_BUTTON1, &BezierCurve::DrawBezierSurface)
END_MESSAGE_MAP()
曲线绘制相关函数实现
// 在OnPaint()中调用绘制曲线的函数
void BezierCurve::OnPaint()
{
DrawBezierCurve();
}
// 鼠标左键单击绘制起点、控制点和终点
void BezierCurve::OnLButtonDown(UINT nFlags, CPoint point)
{
if (IsCompleted)
return;
else
{
points.push_back(point);
// 若不是第一个点,则连接当前点和上一个点
if (points.size() > 1)
{
CStatic* CANVAS = (CStatic*)GetDlgItem(IDC_STATIC_CANVAS);
CDC* pDC = CANVAS->GetDC();
pDC->MoveTo(points[points.size() - 2]);
pDC->LineTo(point);
ReleaseDC(pDC);
}
}
}
// 右键单击表示绘制完毕,将IsCompleted设置为true,阻止后续对左键单击的响应
void BezierCurve::OnRButtonDown(UINT nFlags, CPoint point)
{
IsCompleted = true;
return;
}
// 计算每一个曲线上的点
CPoint BezierCurve::BezierCurvePoint(CPoint p0, CPoint p1, CPoint p2, double t)
{
double x = 0.0;
double y = 0.0;
x = (1 + t * t - 2 * t) * p0.x + 2 * (t - t * t) * p1.x + t * t * p2.x;
y = (1 + t * t - 2 * t) * p0.y + 2 * (t - t * t) * p1.y + t * t * p2.y;
CPoint point((int)x, (int)y);
return point;
}
// 绘制曲线
void BezierCurve::DrawBezierCurve()
{
if (IsCompleted && points.size() >= 3)
{
double t = 0.0;
// 获取绘图区
CStatic* CANVAS = (CStatic*)GetDlgItem(IDC_STATIC_CANVAS);
CDC* pDC = CANVAS->GetDC();
CPoint tempPoint = BezierCurvePoint(points[0], points[1], points[2], t);
CPoint oldPoint = tempPoint;
while (t <= 1)
{
tempPoint = BezierCurvePoint(points[0], points[1], points[2], t);
pDC->MoveTo(oldPoint);
pDC->LineTo(tempPoint);
oldPoint = tempPoint;
t += 0.01;
}
ReleaseDC(pDC);
}
}
曲面绘制相关函数实现
// 随机产生16个控制点
void BezierCurve::GenerateControlPoints()
{
// 获取绘图区的大小,以免控制点超出绘图区范围
CRect Area;
GetDlgItem(IDC_STATIC_CANVAS_SURFACE)->GetClientRect(Area);
int width = Area.Width();
int height = Area.Height();
// 随机种子
std::srand(static_cast<unsigned int>(std::time(0)));
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
int x = std::rand() % (width + 1);
int y = std::rand() % (height + 1);
controlPoints[i][j] = CPoint(x, y);
}
}
}
// 实现组合数计算的函数
int BezierCurve::Combination(int n, int k)
{
if (k == 0 || k == n)
return 1;
else
return Combination(n - 1, k - 1) + Combination(n - 1, k);
}
CPoint BezierCurve::BezierSurfacePoint(float u, float v)
{
float x = 0.0;
float y = 0.0;
// 计算双三次贝塞尔曲面上的点坐标
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
float coef = float(Combination(3, i) * Combination(3, j) * pow(u, i) * pow(1 - u, 3 - i) * pow(v, j) * pow(1 - v, 3 - j));
x += controlPoints[i][j].x * coef;
y += controlPoints[i][j].y * coef;
}
}
return CPoint(x, y);
}
void BezierCurve::DrawBezierSurface()
{
// 获取绘图区
CStatic* CANVAS = (CStatic*)GetDlgItem(IDC_STATIC_CANVAS_SURFACE);
CDC* pDC = CANVAS->GetDC();
// 产生随机控制点
GenerateControlPoints();
// 计算曲面上的所有点并存储,便于后续绘制面片
int i = 0, j = 0;
for (int i = 0; i < 50; i++)
{
for (int j = 0; j < 50; j++)
{
float u = i / 50.0; // 将i映射到范围[0, 1]
float v = j / 50.0; // 将j映射到范围[0, 1]
surfacePoints[i][j] = BezierSurfacePoint(u, v);
}
}
// 绘制面片
for (i = 1; i < 50; i++)
{
for (j = 1; j < 50; j++)
{
pDC->MoveTo(surfacePoints[i - 1][j - 1]);
pDC->LineTo(surfacePoints[i - 1][j]);
pDC->LineTo(surfacePoints[i][j]);
pDC->LineTo(surfacePoints[i][j - 1]);
}
}
ReleaseDC(pDC);
}
实验结果
二次Bezier曲线
双三次Bezier曲面
实验心得
在绘制Bezier曲面时,我发现函数DrawBezierSurface()
中计算曲面点的部分需要将i,j
映射到u,v
,如下:
int i = 0, j = 0;
for (int i = 0; i < 50; i++)
{
for (int j = 0; j < 50; j++)
{
float u = i / 50.0; // 将i映射到范围[0, 1]
float v = j / 50.0; // 将j映射到范围[0, 1]
surfacePoints[i][j] = BezierSurfacePoint(u, v);
}
}
如果将u,v
映射到i,j
(如下),就会出现每一个surfacePoints[12][j]
都为(0,0)
的现象,我不太明白为什么?
而且我发现,即使我修改精度,例如u += 0.01
,仍然会有每一个surfacePoints[12][j]
都为(0,0)
的现象。
int i = 0, j = 0;
for (float u = 0; u <= 1.0; u += 0.02)
{
for (float v = 0; v <= 1.0; v += 0.02)
{
CPoint point = BezierSurfacePoint(u, v);
i = 50 * u;
j = 50 * v;
surfacePoints[i][j] = point;
}
}

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)