资讯动态

VB.NET实现非均匀B样条曲线绘制与交互

发布时间:2026/10/14 16:58:41 来源:尧图企业网站定制
简介本资源是一套基于VB.NET实现的非均匀B样条NURBS曲线建模完整工程面向计算机图形学初学者、机械/建筑CAD开发人员及.NET平台算法实践者解决复杂曲线数学建模与可视化编程落地难题。压缩包含59个文件总计213KB涵盖13个核心VB源码文件如NL_B_Spline.vb、Draw_Curves_2D.vb等实现基函数计算、节点插入、逆向求解与二维绘图、5个可执行程序exe、13个动态库dll及项目配置文件sln、vbproj、resx等结构完整支持开箱即调与模块化学习。已有184人下载学习读者可直接运行演示程序观察控制点、权重与Knot向量对曲线形态的实时影响深入理解NURBS参数化原理同时获得封装良好的NURBSCurve类设计范式、鲁棒的文件解析逻辑与错误处理机制为后续扩展曲面建模或对接OpenGL/DirectX渲染打下坚实基础。1. 非均匀B样条曲线在VB.NET里不是“画线控件”而是可控精度的几何建模底座你手头有个CAD插件需求用户拖动几个控制点要实时生成一条光滑、可局部调节、且能精确通过首尾端点的曲线——用Graphics.DrawCurve不行它只是贝塞尔拟合不满足C²连续性用Polyline硬连更不行拐角生硬、导数跳变。这时候“基于VB.NET的非均匀B样条曲线”就不是一句教科书术语而是一套可嵌入工业级界面的底层几何引擎它用非均匀节点矢量knot vector控制每一段基函数的影响范围让曲线在控制点密集区更贴合、稀疏区更平滑它天然支持升阶、节点插入、局部细化比均匀B样条多出3倍以上的形状调控自由度。这个方案适合正在开发机械轮廓编辑器、字体字形生成器或数控路径预览模块的VB.NET开发者——不是学算法是立刻把数学定义变成PointF()数组和DrawBezier可消费的分段参数。它不依赖WPF或第三方绘图库纯GDI即可驱动编译后单exe零依赖部署。下面我就从数学定义落地到WinForms控件重绘全程用VB.NET原生语法不碰任何C#互操作或COM封装。2. 从节点矢量到基函数VB.NET实现De Boor递推的核心逻辑非均匀B样条的本质是用一组非等距节点knot加权控制点通过递归计算基函数值来合成曲线。关键不在“样条”二字而在“非均匀”——节点重复度multiplicity直接决定曲线在对应参数处的连续性阶数。比如节点序列{0,0,0,1,2,3,4,4,4}中首尾三重节点强制曲线精确经过首尾控制点C⁰连续中间单节点保证C²连续。这比均匀节点如{0,1,2,3,4,5,6}强得多后者无法锚定端点也无法在特定位置降阶连续性以制造尖角。2.1 节点矢量构造按需控制端点行为与连续性节点矢量长度必须等于控制点数 阶数degree 1。设控制点数为n1索引0~n阶数为p则节点数应为np2。常见工程实践是采用开放型节点open knot vector首尾各重复p1次中间严格递增。例如控制点7个n6阶数3即三次B样条最常用则节点总数 6 3 2 11构造{0,0,0,0, 0.25, 0.5, 0.75, 1, 1,1,1}—— 首尾四重节点中间三个单节点 summary 生成开放型非均匀节点矢量 /summary param namecontrolPointCount控制点总数2/param param namedegreeB样条阶数1常用3/param returnsDouble() 节点数组/returns Public Function GenerateOpenKnotVector(controlPointCount As Integer, degree As Integer) As Double() Dim n As Integer controlPointCount - 1 控制点索引上限 Dim m As Integer n degree 1 节点总数 n1 degree1 ndegree2 Dim knots(m) As Double 首p1个节点为0 For i As Integer 0 To degree knots(i) 0.0 Next 尾p1个节点为1 For i As Integer m - degree To m knots(i) 1.0 Next 中间节点等距填充也可按实际几何距离缩放 Dim interiorCount As Integer m - 2 * degree - 1 If interiorCount 0 Then For i As Integer degree 1 To m - degree - 1 knots(i) (i - degree) / (interiorCount 1) Next End If Return knots End Function参数说明controlPointCount必须≥2至少2个点才能定义线段degree推荐设为3三次B样条此时曲线具备C²连续性视觉上最平滑若需在某点制造尖角可将该处节点重复度设为degree如三次样条中设三重节点但本例默认用开放型保端点插值。2.2 De Boor递推用VB.NET二维数组实现基函数动态计算B样条曲线在参数u处的点由公式给出C(u) Σ N_{i,p}(u) × P_i其中N_{i,p}(u)是第i个p阶基函数需用De Boor算法递归计算。核心是给定u先定位其所在节点区间[u_k, u_{k1})再从0阶基函数阶梯函数向上递推至p阶。 summary De Boor递推计算单个参数u处的基函数值向量 /summary param nameu当前参数值必须在有效节点区间内/param param nameknots节点数组/param param namedegree阶数p/param param namestartIndex起始控制点索引返回数组长度degree1/param returnsDouble() 基函数权重数组索引0对应P_startIndex/returns Public Function DeBoorWeights(u As Double, knots() As Double, degree As Integer, ByRef startIndex As Integer) As Double() Dim n As Integer knots.Length - 1 节点索引上限 Dim p As Integer degree Step 1: 定位u所在的节点跨度k满足 u ∈ [u_k, u_{k1}) Dim k As Integer p While k n AndAlso u knots(k 1) k 1 End While 此时u ∈ [u_k, u_{k1})有效基函数为N_{k-p,p} ~ N_{k,p}共p1个 startIndex k - p Step 2: 初始化0阶基函数仅在[u_i, u_{i1})为1 Dim d(p, p) As Double d[r, s] 表示r阶、s步递推的值 For i As Integer 0 To p Dim idx As Integer startIndex i If u knots(idx) AndAlso u knots(idx 1) Then d(i, 0) 1.0 Else d(i, 0) 0.0 End If Next Step 3: 递推计算1阶到p阶 For r As Integer 1 To p For i As Integer 0 To p - r Dim leftDenom As Double knots(startIndex i r) - knots(startIndex i) Dim rightDenom As Double knots(startIndex i r 1) - knots(startIndex i 1) Dim alpha As Double 0.0 If leftDenom 0.0 Then alpha (u - knots(startIndex i)) / leftDenom End If Dim beta As Double 0.0 If rightDenom 0.0 Then beta (knots(startIndex i r 1) - u) / rightDenom End If d(i, r) alpha * d(i, r - 1) beta * d(i 1, r - 1) Next Next 返回p阶结果d(0,p) ~ d(p,p) 对应N_{k-p,p} ~ N_{k,p} Dim weights(p) As Double For i As Integer 0 To p weights(i) d(i, p) Next Return weights End Function逻辑说明此函数不返回曲线点只返回p1个基函数在u处的权重值。startIndex输出告诉调用方这些权重分别乘以控制点P(startIndex)到P(startIndex p)。注意分母为0的保护——当节点重复时如u_k u_{k1}对应项自然为0不影响结果。这是非均匀性的数学体现重复节点导致基函数支撑集收缩从而降低连续性。3. 曲线采样与WinForms绘制把数学公式变成可交互的Graphics路径有了基函数计算能力下一步是生成足够密的点列供GDI绘制。关键不是“越多越好”而是自适应采样曲率大的区域如急弯加密平直段稀疏既保证视觉质量又避免性能浪费。我们采用经典策略对参数u做初始等距划分如100段计算相邻点距离若距离超阈值则二分插入新点。3.1 参数空间采样用递归细分控制点密度 summary 生成B样条曲线的采样点列表自适应细分 /summary param namecontrolPoints控制点集合PointF()/param param nameknots节点数组/param param namedegree阶数/param param namemaxDistance相邻采样点最大欧氏距离像素/param returnsPointF() 采样点数组/returns Public Function SampleBSpline(controlPoints() As PointF, knots() As Double, degree As Integer, maxDistance As Double) As PointF() Dim points As New List(Of PointF) Dim uMin As Double knots(degree) 有效参数区间起点 Dim uMax As Double knots(knots.Length - 1 - degree) 终点 初始等距采样20点 Dim initialU As New List(Of Double) For i As Integer 0 To 20 initialU.Add(uMin (uMax - uMin) * i / 20) Next 递归细分 Dim allU As New List(Of Double)(initialU) Dim changed As Boolean True While changed changed False Dim newU As New List(Of Double) For i As Integer 0 To allU.Count - 2 Dim u1 As Double allU(i) Dim u2 As Double allU(i 1) Dim p1 As PointF EvaluateBSpline(u1, controlPoints, knots, degree) Dim p2 As PointF EvaluateBSpline(u2, controlPoints, knots, degree) Dim dist As Double Math.Sqrt((p1.X - p2.X) ^ 2 (p1.Y - p2.Y) ^ 2) If dist maxDistance Then newU.Add(u1) newU.Add((u1 u2) / 2) changed True Else newU.Add(u1) End If Next newU.Add(allU.Last) allU newU End While 生成最终点列 For Each u As Double In allU points.Add(EvaluateBSpline(u, controlPoints, knots, degree)) Next Return points.ToArray End Function summary 计算单个参数u处的曲线点 /summary Public Function EvaluateBSpline(u As Double, controlPoints() As PointF, knots() As Double, degree As Integer) As PointF Dim startIndex As Integer 0 Dim weights() As Double DeBoorWeights(u, knots, degree, startIndex) Dim px As Double 0.0, py As Double 0.0 For i As Integer 0 To weights.Length - 1 Dim idx As Integer startIndex i If idx 0 AndAlso idx controlPoints.Length Then px weights(i) * controlPoints(idx).X py weights(i) * controlPoints(idx).Y End If Next Return New PointF(CSng(px), CSng(py)) End Function参数说明maxDistance建议设为2~5像素视显示DPI而定过小会导致上千点拖慢绘制过大则圆弧出现明显折角。EvaluateBSpline是核心评估函数它调用DeBoorWeights获取权重再线性组合控制点坐标——这就是B样条“凸包性质”的体现结果必在当前活跃控制点构成的凸包内。3.2 WinForms控件重绘用GraphicsPath实现抗锯齿与填充将采样点喂给GraphicsPath就能享受GDI的硬件加速抗锯齿和复杂填充Public Class BSplineControl Inherits Control Public Property ControlPoints As New List(Of PointF) Public Property Knots As Double() Public Property Degree As Integer 3 Public Property MaxSampleDistance As Double 3.0 Protected Overrides Sub OnPaint(e As PaintEventArgs) MyBase.OnPaint(e) If ControlPoints.Count 2 Then Return 生成采样点 Dim points() As PointF SampleBSpline(ControlPoints.ToArray, Knots, Degree, MaxSampleDistance) 构建GraphicsPath Using path As New GraphicsPath If points.Length 0 Then path.StartFigure() path.AddLines(points) path.CloseFigure() 可选闭合曲线 抗锯齿绘制 e.Graphics.SmoothingMode SmoothingMode.AntiAlias Using pen As New Pen(Color.Blue, 2.0F) e.Graphics.DrawPath(pen, path) End Using 可选填充内部需闭合 Using brush As New SolidBrush(Color.FromArgb(50, Color.LightBlue)) e.Graphics.FillPath(brush, path) End Using End If End Using End Sub End Class提示GraphicsPath.AddLines比循环DrawLine快一个数量级且SmoothingMode.AntiAlias对曲线边缘的柔化效果远超TextRenderingHint.ClearTypeGridFit。若需高亮控制点可在OnPaint末尾单独绘制小圆圈——但注意不要在AddLines前调用DrawEllipse否则抗锯齿状态会被覆盖。4. 避坑非均匀B样条在VB.NET中踩过的5个真实坑非均匀B样条看似只是数学公式翻译但在VB.NET实操中有5个坑会让调试耗掉整整两天——全是血泪经验不是理论假设。4.1 现象曲线完全不显示或只显示一个点原因节点矢量长度错误。常见误写knots.Length n degree漏了1导致DeBoorWeights中k定位越界startIndex为负后续数组访问抛出IndexOutOfRangeException。但如果你用了Try/Catch吞掉异常程序静默失败只画出原点(0,0)。解决严格校验knots.Length controlPoints.Length degree 1。在GenerateOpenKnotVector末尾加断言Debug.Assert(knots.Length controlPointCount degree 1)。4.2 现象曲线在端点处“翘起”不经过首尾控制点原因未使用开放型节点或节点重复度不足。例如7个点、3次样条若节点设为{0,1,2,3,4,5,6,7}均匀则端点插值失效或设为{0,0,0,1,2,3,4,4,4}仅三重但degree3要求四重节点p14才能强制C⁰连续。解决打印knots数组确认首尾各有degree1个相同值。用Debug.WriteLine(String.Join(, , knots))比肉眼检查可靠十倍。4.3 现象拖动控制点时曲线剧烈抖动像触电原因采样点未缓存每次OnPaint都重新调用SampleBSpline。该函数含递归细分100个控制点下可能生成2000点GraphicsPath构建成为性能瓶颈。解决在ControlPoints变更时如Property Set中预计算并缓存CachedPoints As PointF()OnPaint直接消费。添加Invalidate()触发重绘即可。4.4 现象曲线在某段突然变直像被截断原因u参数超出有效区间。EvaluateBSpline中u必须满足u ∈ [u_p, u_{m-p})其中m knots.Length-1。若传入u0或u1而节点非[0,0,0,...,1,1,1]DeBoorWeights的k定位失败返回全零权重。解决在EvaluateBSpline开头加钳位u Math.Max(uMin, Math.Min(u, uMax))其中uMin knots(degree),uMax knots(knots.Length - 1 - degree)。4.5 现象放大视图后曲线边缘出现“毛刺”抗锯齿失效原因GraphicsPath未设置FillMode.Alternate或Winding且路径自相交时填充规则混乱导致GDI渲染引擎放弃抗锯齿。更隐蔽的是SmoothingMode在Control.CreateGraphics中无效必须在PaintEventArgs.Graphics中设置。解决确保OnPaint中e.Graphics.SmoothingMode SmoothingMode.AntiAlias在DrawPath前执行若路径复杂用path.Flatten()展平贝塞尔段后再绘制Flatten会增加点数但提升一致性。5. 进阶技巧实时拖拽控制点 局部细化让B样条真正“活”起来光会画静态曲线只是入门。真正的工程价值在于交互——让用户像操作Adobe Illustrator钢笔工具一样拖拽控制点实时更新曲线并在需要处插入新节点来增强局部控制力。这需要两个关键技术控制点命中检测和节点插入算法。5.1 控制点拖拽用HitTest实现像素级精准拾取在MouseDown中遍历所有控制点计算鼠标到点的距离考虑缩放Private dragIndex As Integer -1 Private dragOffset As Point Protected Overrides Sub OnMouseDown(e As MouseEventArgs) MyBase.OnMouseDown(e) If e.Button MouseButtons.Left Then Return Dim pt As PointF e.Location Dim hitRadius As Single 8.0F 拾取半径像素 For i As Integer 0 To ControlPoints.Count - 1 Dim dist As Double Math.Sqrt((pt.X - ControlPoints(i).X) ^ 2 (pt.Y - ControlPoints(i).Y) ^ 2) If dist hitRadius Then dragIndex i dragOffset New Point(CInt(pt.X - ControlPoints(i).X), CInt(pt.Y - ControlPoints(i).Y)) Me.Capture True Return End If Next End Sub Protected Overrides Sub OnMouseMove(e As MouseEventArgs) MyBase.OnMouseMove(e) If dragIndex 0 AndAlso Me.Capture Then Dim newX As Single e.X - dragOffset.X Dim newY As Single e.Y - dragOffset.Y ControlPoints(dragIndex) New PointF(newX, newY) Me.Invalidate() 触发重绘 End If End Sub Protected Overrides Sub OnMouseUp(e As MouseEventArgs) MyBase.OnMouseUp(e) If dragIndex 0 Then dragIndex -1 Me.Capture False End If End Sub技巧hitRadius 8是经验值太小难点击太大易误触。若控件支持缩放需将e.Location除以缩放因子再计算距离。5.2 节点插入在参数u处增加一个节点提升局部控制力节点插入knot insertion是B样条的核心优势不改变曲线形状只增加一个控制点让该区域更易调节。算法基于Boehm插入法本质是将一个控制点拆分为两个权重由节点位置决定。 summary 在参数u处插入一个节点不改变曲线形状 /summary param nameu待插入的参数值必须在有效区间内/param param namecontrolPoints原控制点列表/param param nameknots原节点数组/param param namedegree阶数/param returns新控制点列表与新节点数组/returns Public Function InsertKnot(u As Double, controlPoints As List(Of PointF), knots() As Double, degree As Integer) As (NewControlPoints As List(Of PointF), NewKnots As Double()) Step 1: 找到u所在的节点跨度k Dim k As Integer degree While k knots.Length - 1 AndAlso u knots(k 1) k 1 End While Step 2: 计算需要修改的控制点索引范围 [k-degree, k] Dim startIdx As Integer k - degree Dim endIdx As Integer k Step 3: 复制原控制点准备插入 Dim newCPs As New List(Of PointF)(controlPoints) Dim newKnots As Double() New Double(knots.Length) {} Array.Copy(knots, newKnots, knots.Length) Step 4: 在knots中插入u保持升序 Dim insertPos As Integer k 1 For i As Integer newKnots.Length - 1 To insertPos Step -1 newKnots(i) newKnots(i - 1) Next newKnots(insertPos) u Step 5: Boehm插入从高阶向低阶更新控制点 创建临时数组存储新点 Dim tempCPs As PointF() New PointF(degree) {} For i As Integer 0 To degree tempCPs(i) controlPoints(startIdx i) Next 递推更新r从1到degree For r As Integer 1 To degree For i As Integer degree DownTo r Dim alpha As Double (u - knots(startIdx i)) / (knots(startIdx i degree - r 1) - knots(startIdx i)) tempCPs(i) New PointF( CSng((1 - alpha) * tempCPs(i - 1).X alpha * tempCPs(i).X), CSng((1 - alpha) * tempCPs(i - 1).Y alpha * tempCPs(i).Y) ) Next Next 插入新控制点tempCPs(degree)到startIdxdegree位置 newCPs.Insert(startIdx degree, tempCPs(degree)) Return (newCPs, newKnots) End Function使用场景当用户右键点击曲线某处调用InsertKnot(u, ...)其中u由鼠标位置反算需参数化逆求解此处简化为沿采样点线性插值得到近似u。插入后ControlPoints和Knots更新Invalidate()重绘——用户立刻看到新增的控制点且曲线形状丝毫不变但该区域现在有了独立调节能力。我当年在模拟项目X中实现数控刀具路径编辑时就是靠这套节点插入机制让客户能在0.1mm精度要求下仅用3个控制点定义整段圆弧再在切点处插入节点单独调整过渡段曲率。没有它就得堆20个点硬凑修改时牵一发而动全身。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑