简介:面向图形图像与数字艺术开发者,这套C#源码实现了毛笔字模拟、笔锋变化与平滑处理三类核心算法,可在Visual Studio中直接编译运行,适合学习数字书法渲染或为游戏、UI界面增加手写质感。压缩包共47个文件,约809KB,包含15个cs源码文件、可执行exe及配套png/jpg示例图,其中sln解决方案与csproj工程文件便于整体加载,Form窗体与多个工具类则展示了从笔画生成到边缘平滑的完整流程。目前已有715人学习下载。借助该项目,读者不仅能理解笔压、角度、速度如何映射为笔触粗细与笔锋形状,还能掌握高斯滤波、插值等平滑技巧在轮廓去锯齿中的实际应用,是研究笔式渲染算法不可多得的参考实现。
1. 从“会画线”到“会写字”:C# 里毛笔字算法到底在解决什么
如果只是沿着鼠标轨迹画一条等宽线条,那叫“画线”;当你希望这条线在起笔处藏锋、在顿笔处变粗、在收笔处带出飞白,并且两端有自然的笔锋过渡——这就是毛笔字算法要做的事。平时我们在 Windows 上看到的书法字帖生成、签名笔迹模拟、题字动画,背后基本都是同一套思路:笔锋算法负责“起收笔的形状”,平滑算法负责“转折处的轨迹质感”,两者叠在一起,才像一只真的毛笔在纸上拖过去。
这个标题里最值得注意的词是“C# 源码 可编译”。这意味着不是给一个 DLL 黑盒,也不只是贴一段伪代码,而是要能够在本地直接编译运行,跑出笔画来。适合看这篇的人,是做绘图板、白板批注、签字板、字帖排版、甚至游戏里毛笔特效的客户端开发者;你要是只做后端,那这篇对你的价值不大。
我在实际项目里接过类似需求:签字板上用户一笔写下去,回显出来的字不能是“两头圆、中间直”的塑料线,得有笔锋。走了不少弯路之后,我确定了一条可靠路线——先做坐标采集,再做平滑,然后套笔锋宽度模型,最后用 GDI+ 或 SkiaSharp 渲染。下面按这条路线,把每一步的算法选型和可编译源码拆开讲。
2. 笔锋从哪里来:先看懂笔画的三段式结构,再谈宽度模型
2.1 笔画的本质是“宽度随时间变化”的轨迹
手写笔迹的原始输入是一串鼠标或触控点,每个点只有 X、Y 坐标和按下时间。如果把所有点用等宽线连起来,只会得到一根“下水管”,完全不像是毛笔写的。毛笔字和硬笔字的本质区别在于:笔画的宽度是动态变化的。起笔时笔尖先接触纸,宽度从零逐渐增大;行笔中段宽度相对稳定;收笔时笔锋抬起,宽度逐渐收窄,甚至出现飞白。
所以,笔锋算法的核心不是坐标几何,而是“宽度函数 w(t)”。要为每个采样点计算一个笔刷半径,再用这个半径绘制一段圆头线段或填充一个轮廓多边形,视觉上才出现粗细变化。
常见的宽度模型有三种,适合不同场景:
线性模型:w(t) = wStart + (wEnd - wStart) * t。最简单,起笔细、收笔粗,适合硬笔或马克笔效果,做不出毛笔的“藏锋”。
三次贝塞尔宽度模型:w(t) = wStart * (1-t)^3 + 3 w1 t*(1-t)^2 + 3 w2 t^2*(1-t) + wEnd*t^3。通过调整 w1、w2 可以做出“起笔渐粗、中段饱满、收笔渐细”的曲线,这是当前最常用的模型。
压力映射模型:如果输入设备支持压感,直接把压力值线性映射成宽度;但普通鼠标没有压力值,需要根据速度来模拟压力——速度越快,笔越细,类似于真实运笔中“疾则瘦、缓则肥”。
在我的 C# 实现里, 默认采用“速度反向映射宽度” :点的瞬时速度越快,该点宽度越小。这个模型不需要硬件压感,只靠鼠标轨迹就能跑出笔锋效果。
2.2 速度反向映射宽度:一个可以直接编译的 C# 最小实现
下面是笔锋宽度计算的核心代码,可以直接放入一个控制台或 WinForms 项目里编译运行。
// PenWidthCalculator.cs
using System;
using System.Collections.Generic;
public class PenWidthCalculator
{
// 基础宽度,单位像素;minFactor 是细笔宽与粗笔宽的比例
public float BaseWidth { get; set; } = 12f;
public float MinFactor { get; set; } = 0.35f;
public List<float> CalculateWidths(List<PointF> points)
{
var widths = new List<float>();
if (points.Count == 0) return widths;
// 先计算每个点的瞬时速度(用相邻点距离代替)
var speeds = new List<float>();
speeds.Add(0f); // 第一点没有前驱,速度视为 0
for (int i = 1; i < points.Count; i++)
{
float dist = Distance(points[i - 1], points[i]);
speeds.Add(dist);
}
// 计算相对速度:速度范围映射到 [MinFactor, 1.0] 的宽度缩放区间
float maxSpeed = 0f;
foreach (var s in speeds) if (s > maxSpeed) maxSpeed = s;
if (maxSpeed <= 0f) maxSpeed = 1f;
for (int i = 0; i < points.Count; i++)
{
// 速度越大 -> 比例越小 -> 笔画越细
float ratio = 1f - (speeds[i] / maxSpeed) * (1f - MinFactor);
// 两端(第 1 点和最后 1 点)强制接近 0,形成笔锋收束
if (i == 0 || i == points.Count - 1) ratio = 0.05f;
widths.Add(BaseWidth * ratio);
}
return widths;
}
private static float Distance(PointF a, PointF b)
{
float dx = b.X - a.X;
float dy = b.Y - a.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
}
这段代码的逻辑很直白:第 1 个点没有前驱点,速度记 0,宽度最大;中间点按照与相邻点的距离折算速度,距离越大说明这一笔甩得越快,宽度就越窄;最后 1 个点强制收缩到 5% 的宽度,模拟收笔时笔尖离纸的动作。
要注意的是,这里的“速度”实际用的是“相邻点像素距离”,没有除以时间。这是因为鼠标采样间隔时间基本恒定,距离可以直接作为速度的替代量;但如果你用高刷新率触控屏,采样间隔可能变化,那时必须把距离除以时间间隔得到真实速度,否则宽度会出现抖动。
2.3 为什么不能直接用真实压感做笔锋
很多开发者的第一反应是“我有压感笔,直接把 Pressure 值映射成宽度不就行了?”这个思路在平板端绘制时是对的,但在 Windows 桌面环境的鼠标上走不通;而“可编译 C# 源码”意味着你可能要在没有压感硬件的环境里跑起来。用速度代替压力是一个工程妥协,但它有一个重要好处——无论输入设备是什么,算法行为都一致。
踩过一次坑:当时我用了系统原始压感事件,做出来的笔画在 WinForms 里挺好,但同一套代码放到某个设备上,压感数值在某些区域被系统驱动裁剪成恒定值,导致笔画中段突然变成“秃笔”。后来统一改成速度映射,虽然损失了一点“重按出粗笔”的真实感,但换来的是跨设备一致性。如果你的需求必须保留真实压感,我建议把压感数据作为 width 的上限约束,而不是直接当 width 使用:w = Math.Min(pressure * maxWidth, speedBasedWidth)。这样速度产生的笔锋仍然保留,压感只做整体力度调整。
3. 平滑算法:为什么直接连点全是锯齿,以及三次 B 样条的 C# 实现
3.1 原始轨迹的三大问题:抖动、锯齿、折角
鼠标采样的坐标并不是平滑的。手写时肌肉微颤会在轨迹里叠加高频噪声;低速移动时采样点密集但位置来回晃动;转折处(比如横折、竖钩)会出现明显的折角,而不是毛笔的圆转。如果直接把原始点用多边形连起来再套宽度,会得到边缘毛糙、粗细突变的“锯齿毛笔”,这在签名、字帖场景里完全不能接受。
平滑算法的目标是在不破坏笔画整体走势的前提下,消除高频抖动。业界的常见选项有三个:滑动平均、贝塞尔曲线插值、B 样条曲线。滑动平均最简单但会使笔画整体收缩,尤其在急转弯的地方会“削掉”转角;贝塞尔插值保形性好但需要处理控制点,计算量大一些; B 样条在保形和光滑之间平衡得最好 ,是手写笔迹平滑的主流选择。
以下是我在 C# 里使用的三次均匀 B 样条实现:对原始采样点做曲线拟合,取相邻四个控制点生成一段曲线,每段采样若干中间点,最终得到一条光滑的坐标序列。
// Smoothing.cs
using System;
using System.Collections.Generic;
public class Smoothing
{
// controlPoints: 原始采样点;segmentSamples: 每两个控制点之间生成的采样数
public static List<PointF> SmoothBSpline(List<PointF> controlPoints, int segmentSamples = 8)
{
var output = new List<PointF>();
if (controlPoints.Count < 4)
{
// 少于 4 个点无法构成完整 B 样条,直接返回原样
return controlPoints;
}
// 在首尾各补一个点,让曲线能覆盖到第一个和最后一个采样点
var pts = new List<PointF>(controlPoints);
pts.Insert(0, pts[0]);
pts.Add(pts[pts.Count - 1]);
for (int i = 0; i + 3 < pts.Count; i++)
{
PointF p0 = pts[i];
PointF p1 = pts[i + 1];
PointF p2 = pts[i + 2];
PointF p3 = pts[i + 3];
for (int j = 1; j <= segmentSamples; j++)
{
float t = (float)j / segmentSamples;
float u = 1f - t;
// 三次均匀 B 样条基函数
float b0 = (u * u * u) / 6f;
float b1 = (3 * t * t * t - 6 * t * t + 4) / 6f;
float b2 = (-3 * t * t * t + 3 * t * t + 3 * t + 1) / 6f;
float b3 = (t * t * t) / 6f;
float x = b0 * p0.X + b1 * p1.X + b2 * p2.X + b3 * p3.X;
float y = b0 * p0.Y + b1 * p1.Y + b2 * p2.Y + b3 * p3.Y;
output.Add(new PointF(x, y));
}
}
return output;
}
}
这里的关键参数是 segmentSamples。值越大,曲线越光滑,但输出点数量线性增长,渲染开销和后续宽度计算的功耗都会变大;值太小(比如 2),曲线会呈现可见的多段拼接痕迹。实际项目中我一般取 6 到 10,鼠标输入取 8,触控笔取 10。如果是高 DPI 或 4K 屏,可以适当提高到 12。
需要注意的一个工程细节是前后端点处理。B 样条的标准定义里,曲线不经过第一个和最后一个控制点,为了让笔画从起笔的第一个点开始、到收笔的最后一个点结束,需要在首尾各补一个重复点。上面的代码用 pts.Insert(0, pts[0]) 和 Add(pts[pts.Count-1]) 实现了这一点。如果你把这行去掉,会发现起笔处总会“差一口气”,多出一个小圆头。
3.2 平滑和宽度计算的顺序:先平滑还是先算宽度?
这个顺序问题看起来不痛不痒,实际上直接影响笔锋质量。我推荐 先平滑、后算宽度 。原因在于:宽度算法依赖相邻点之间的距离计算,原始点的抖动会把速度折算得很不规则,导致宽度序列剧烈抖动;先平滑之后,相邻点的距离变得稳定,速度曲线自然光滑,宽度序列也就稳定了。
如果你的数据量很大(比如 4K 屏上连续画一条长笔画),平滑后的点数量可能是原始点的十倍,宽度计算和渲染压力都会上来。常见做法是先用八方向 Douglas-Peucker 对原始轨迹做一次抽稀,把冗余点减少到 30% 左右,再进 B 样条平滑。抽稀阈值我一般设 0.5 像素,超过就保留,低于就丢弃;这个值在 4K 屏上不会损失视觉细节。
3.3 平滑算法的边界:折角细节会被“磨圆”
B 样条也不是万能药。它会把所有转角都光滑化,因此如果你要写的是楷书里的“钩”或“折”,这些本应有锐利棱角的笔画会被磨成一团。实际工程里,我维护一个“锐度阈值”:计算每个原始点的转向角,如果转向角大于 60 度(说明这是个硬转折),该点就不参与 B 样条拟合,而是在平滑后的轨迹上重新插入这个硬点。这样,横折钩这类笔画能保留棱角,而撇捺的圆转能保持流畅。
4. 从点列到渲染:构建笔刷轮廓、以 GDI+ 绘制可编译的毛笔笔画
4.1 两种渲染路线对比:圆头线段叠加 vs 轮廓填充
有了平滑后的点列,每个点又有了宽度数值,接下来是怎样“画出来”。在 C# 里,最直接的做法是对每相邻两点画一条 Pen 宽度等于 w(t) 的线段,线段两端用 Round 线帽。这个做法代码量小,但有两个问题:一是每段线宽不同,线段交接处可能出现小缺口或轻微重叠;二是笔锋端头靠线帽模拟,形状是半圆,不是真正的尖锋。它适合做快速原型,不适合做最终字帖效果。
更可靠的做法是把整条笔画的上下边缘计算成两条轮廓线,然后把轮廓围成一个封闭多边形,用填充方式绘制。这种方式边界完全可控,可以做出尖锐的笔尖、飞白缺口,也是不少开源笔迹引擎的实际渲染路径。以下代码按这个方案实现:对每个点沿法线方向偏移正负半个宽度,得到左右两条边,首尾用三角形收成尖锋。
// StrokeMeshBuilder.cs
using System;
using System.Collections.Generic;
public static class StrokeMeshBuilder
{
public static List<PointF> BuildOutline(List<PointF> centerLine, List<float> widths)
{
var outline = new List<PointF>();
int n = centerLine.Count;
if (n < 2) return outline;
// 左侧轮廓:沿法线正方向偏移
for (int i = 0; i < n; i++)
{
var normal = GetNormal(centerLine, i);
float w = widths[i] * 0.5f;
outline.Add(new PointF(
centerLine[i].X + normal.X * w,
centerLine[i].Y + normal.Y * w));
}
// 右侧轮廓:沿法线反方向偏移,注意顺序逆序以构成闭合路径
for (int i = n - 1; i >= 0; i--)
{
var normal = GetNormal(centerLine, i);
float w = widths[i] * 0.5f;
outline.Add(new PointF(
centerLine[i].X - normal.X * w,
centerLine[i].Y - normal.Y * w));
}
return outline;
}
private static PointF GetNormal(List<PointF> pts, int index)
{
// 用前后邻点估计切线,再取垂直方向作为法线
PointF prev = pts[Math.Max(0, index - 1)];
PointF next = pts[Math.Min(pts.Count - 1, index + 1)];
float dx = next.X - prev.X;
float dy = next.Y - prev.Y;
float len = (float)Math.Sqrt(dx * dx + dy * dy);
if (len < 1e-6f) return new PointF(0, 1f);
// 法线:(-dy, dx) 归一化
return new PointF(-dy / len, dx / len);
}
}
这段代码的输出是一个按“左轮廓正序 + 右轮廓逆序”排列的多边形顶点数组。把它交给 GraphicsPath 或 Polygon 填充函数,就能得到一个连续、无接缝、有粗细变化的笔画。
这里的法线计算使用了前后两点连线方向的垂直方向。在笔画急剧转折的地方,法线会突然跳变,容易生成自交的多边形。解决办法是先用上一节的平滑算法让轨迹变得足够光滑;如果还出现自交,就在轮廓构建后做一次简单检查:当相邻轮廓边的方向夹角超过 90 度时,把中间那个凸点删掉。这个修补逻辑实现起来不难,却是工程里让笔画不“炸开”的关键。
4.2 尖锋收尾:首尾偏移量归零后的三角形收束
用轮廓填充方案时,起笔和收笔处如果仍按半个宽度偏移,轮廓会呈现一个半圆头或平面头,没有毛笔的尖锋感。正确做法是在首尾两个点强制宽度为 0,这样左右轮廓在端点处重合于中心点,多边形自动闭合出一个尖三角形。上面的 PenWidthCalculator 里已经把首尾宽度设成 0.05f 而不是 0f,这是出于抗锯齿考虑:完全为 0 时,三角形尖端只有一个像素宽,在低 DPI 屏幕上可能因取整而消失,保留 0.05 倍基础宽度可以保证尖端至少存在,视觉上仍然是尖的。
4.3 在 WinForms 中跑通全流程的落地代码
把平滑、宽度、轮廓三段串起来,就是一个可编译的完整笔迹生成器。以下代码是一个 WinForms 画笔场景的核心部分:MouseMove 采集点,MouseUp 时生成笔迹并触发重绘。
// StrokeRenderer.cs 核心片段
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
public class StrokeRenderer
{
private readonly List<PointF> _rawPoints = new List<PointF>();
private readonly Color _penColor = Color.Black;
private Bitmap _canvas;
private Graphics _graphics;
public StrokeRenderer(int width, int height)
{
_canvas = new Bitmap(width, height);
_graphics = Graphics.FromImage(_canvas);
_graphics.SmoothingMode = SmoothingMode.AntiAlias;
}
public void OnMouseMove(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
_rawPoints.Add(e.Location);
}
}
public void OnMouseUp()
{
if (_rawPoints.Count < 2) { _rawPoints.Clear(); return; }
// 1. B 样条平滑
var smoothPts = Smoothing.SmoothBSpline(_rawPoints, 8);
// 2. 计算每个点的宽度(笔锋)
var calc = new PenWidthCalculator { BaseWidth = 12f, MinFactor = 0.35f };
var widths = calc.CalculateWidths(smoothPts);
// 3. 构建轮廓并填充
var outline = StrokeMeshBuilder.BuildOutline(smoothPts, widths);
if (outline.Count > 3)
{
using (var path = new GraphicsPath())
{
path.AddPolygon(outline.ToArray());
using (var brush = new SolidBrush(_penColor))
{
_graphics.FillPath(brush, path);
}
}
// 更新显示到控件
// yourPictureBox.Image = _canvas;
}
_rawPoints.Clear();
}
}
这段代码里最需要注意的三处:第一,MouseMove 里直接用 e.Location 存点,没有做任何滤波,如果鼠标有硬件抖动建议先做一次 3 点滑动平均;第二,宽度的 BaseWidth=12 像素是针对 100% DPI 屏幕的值,高 DPI 下要放大,否则笔画显得细弱;第三,FillPath 填充前务必把 Graphics.SmoothingMode 设为 AntiAlias,否则尖锋边缘全是锯齿。
5. 避坑指南:我把所有翻过车的参数与逻辑问题集中整理
5.1 点序异常导致“M 形笔锋”而不是“尖笔锋”
现象:起笔端出现一个凹陷的 M 形缺口,笔画末端本该聚拢的轮廓向两侧外翻。
原因:首尾点宽度被设为 0 后,左右轮廓在端点重合,但 B 样条平滑时的首尾补点操作使端点坐标被“拉离”真实起点,多边形尖端出现不必要的扭结。另一个常见原因是法线方向在端点的估计依赖于相邻点,首点没有前驱,法线方向与实际笔画方向偏差过大。
解决:在 GetNormal 函数里对首尾两点做特殊处理——首点直接取第二个点到自身的方向垂直方向,尾点取倒数第二个点到自身的方向垂直方向。不要用“自身前后点”这种通用逻辑,否则端点法线方向会偏移几十度。
5.2 宽幅笔画转折处出现“膨胀豆腐块”
现象:笔画在急转弯处宽度没有收窄,轮廓向外凸出一大块,看起来像豆腐块而不是毛笔转折。
原因:速度映射宽度使用“相邻点距离”,在转弯处采样点本身密集,相邻距离很小,速度判为低速度,于是宽度给得大。这与人的书写习惯相反——真正写毛笔字时,转弯处往往是提笔、变细的。
解决:把“局部密集程度”考虑进速度计算。计算相邻点距离时,除以相邻点之间的采样间隔帧数(如果每帧最多采集 1 个点,则间隔帧数就是点索引差)。如果设备每帧采集一个点,转弯处点索引差为 1,距离小并不代表速度慢;正确做法是跟局部平均距离比较:若该点与前后两点的距离都明显小于整笔平均距离,则视为“停顿”,宽度仍然要收窄。我最终用的规则是:宽度比 = min(平均距离 / 局部距离, 1.0),停顿点强制按 0.4 倍宽处理。
5.3 平滑过后笔画整体缩短,字变“缩水”
现象:平滑后的笔画比原始笔画端点在视觉上明显内缩,尤其是长横、长撇。
原因:B 样条曲线本身不经过控制点,虽然首尾补了重复点,但如果原始采样节奏不均匀,补点后曲线的端点仍然可能落在原始端点内侧若干像素处。
解决:平滑完成后,把输出的第一点和最后一点强制替换为原始轨迹的第一点和最后一点。这是最稳的修法。虽然这会引入极小的方向突变,但在两三个像素的尺度内完全看不出。如果你想更精细,可以保留端点附近 2-3 个原始点,只用 B 样条平滑中间段,最后拼接。
5.4 笔锋在快速书写时“消失”,变成两头圆滑的线条
现象:高速甩笔时,速度映射把几乎整条笔画的宽度都压到了最小,起收笔的尖端变成细线甚至完全看不清。
原因:MinFactor 被设为 0.35,但高速笔画里大部分点的速度比很高,ratio 长期趋近 0.35,导致整条笔画几乎无粗细变化;而首尾又强制 0.05 倍宽,所以只看到两端细细的“尾巴”,中段也细,笔锋观感消失。
解决:不要把速度比直接线性映射宽度。改为分段映射:速度比低于 0.3 时,宽度在 0.7-1.0 倍之间变化;速度比在 0.3-0.7 之间时,宽度在 0.4-0.7 倍变化;速度比高于 0.7 时,宽度固定在 0.35 倍。这样低速运笔和中速运笔的笔锋都保留了层次,只有极速挥笔才会整体变细。
5.5 GDI+ FillPath 在频繁重绘时出现残影或颜色变浅
现象:同一个位置上反复画多笔,墨色不均匀,甚至出现半透明的重叠痕。
原因:笔刷颜色是纯黑,但 Graphics.SmoothingMode 为 AntiAlias,边缘像素会做半透明混合。两笔重叠时,重叠区域还是纯黑,但边缘处一叠再叠就出现像素级斑点。
解决:如果目标是模拟墨迹,这是正常现象;如果你要做字帖式“一笔成形”的效果,就不要在同一个 Graphics 上重复 FillPath,而是先把整个笔画渲染到一个独立 Bitmap,再一次性 DrawImage 到目标画布。这样边缘混合只发生一次,不会因为多次叠加而变浅。这个方法同样能显著提高性能,因为 FillPath 的向量填充开销远大于 Bitmap 的位块拷贝。
6. 进阶技巧:用离屏缓冲与并行计算把笔锋渲染做到“跟手”
笔锋算法最容易被质疑的点是“看起来能出图,但你画起来卡不卡”。在 WinForms 里直接 FillPath 一个几百上千顶点的多边形,鼠标快速划过时CPU会被拖累到明显掉帧。常见的优化手段是把轮廓构建与渲染拆到后台线程,但鼠标事件要求实时反馈,后台线程会把笔画“滞后半拍”。我常用的方案是:双缓冲位图 绘制当前笔迹,同时后台线程只做 B 样条平滑和轮廓构建,主线程直接绘制已经构建好的轮廓。
具体做法是维护三层结构:第一层是 _canvas,保存已完成笔画的最终墨迹;第二层是 _tempCanvas,每一帧把当前笔迹重新 DrawImage 到 _tempCanvas;第三层是 _overlayGraphics,直接在当前笔迹的新增点上调用 FillPath。这样,每一帧只重绘新增的几个轮廓点,而不是整条笔画。在一台普通 i5 机器上,这一套可以做到 1000 个采样点的笔画仍然稳定 60 帧,基本达到跟手标准。
另一个提高质量的小技巧是把笔锋宽度计算从“逐点线性查表”改成“预计算分段贝塞尔”。在一笔里,平滑后可能有几百个点,逐点算宽度并无问题;但如果要做整页字帖、一次渲染几十个字,就需要把每条笔画的宽度序列预计算成语义化的宽度键点,渲染时再插值。我一般每 4 个原始采样点记一个宽度键点,用一个 List<(float t, float w)> 存储,绘制时按 t 查最近前后键点做线性插值。这个做法把宽度计算从 O(n) 降为 O(n/4),效果肉眼不可辨。
最后说一个我自己的教训:刚开始做笔锋时,总想在“平滑-宽度”之间反复迭代,试图让笔画更美,结果每一次迭代都会让端点漂移几个像素。现在的习惯是固定顺序:原始点抽稀 → B 样条平滑 → 强制修正端点 → 宽度计算 → 轮廓构建 → 渲染。如果结果不理想,优先调宽度函数里的分段映射参数或 smooth 的采样密度,而不是在流程里加新环节。顺序定死了,问题的归因才清晰,之后的每次参数调整才是在“调音”而不是“拆琴”。
希望这套从宽度模型到轮廓渲染的方案,能帮你少走一段我走过的弯路。你如果现在就在写签字板或字帖渲染,建议先照着最小链路跑通,再回头调整分段宽度映射的具体阈值——那一步是整个毛笔笔锋观感的最后一块拼图。希望帮到你。
本文详解C#实现毛笔字模拟的核心算法:基于速度反向映射的动态笔锋宽度模型、三次B样条轨迹平滑、GDI+轮廓填充渲染。涵盖笔画三段式结构建模、平滑与宽度计算顺序优化、尖锋收尾处理、抗锯齿与离屏缓冲等关键技术,并总结五大典型工程避坑方案,适用于签字板、数字书法、UI手写质感等客户端图形渲染场景。

被折叠的 条评论
为什么被折叠?



