OpenGL毛笔字渲染:几何着色器与飞白纹理实现笔锋效果
简介这份资源聚焦于利用OpenGL实现毛笔字渲染的算法面向具备一定图形学基础与C编程经验的开发者尤其是对非真实感渲染、书法模拟或粒子特效感兴趣的技术人员。它解决的是如何在实时图形环境中还原毛笔笔触的粗细变化、墨迹扩散与运笔轨迹等核心问题属于图形学与艺术效果交叉的进阶实践。压缩包共37个文件约840KB包含5个cpp源文件与6个h头文件构成主要逻辑另有9个bmp与3个jpg提供纹理素材配合ico、rc等资源文件及vc工程配置整体结构完整可直接编译运行。目前已有367人学习下载说明该算法在相关技术圈内具备一定参考价值。读者可从中获取毛笔笔触建模思路、OpenGL纹理与混合渲染技巧以及一套可运行的VC示例工程便于在此基础上二次开发或移植到其他图形框架中。1. 用 OpenGL 写毛笔字从一笔“飞白”说起如果你试过在屏幕上用鼠标画一条线然后试图让它看起来像毛笔写出来的大概率会得到一根粗细均匀、边缘锐利的“铁丝”。这不是你手抖而是默认的 OpenGL 画线逻辑根本不理解毛笔的脾气。毛笔笔迹的核心在于笔锋的提按导致笔迹宽度动态变化加上墨汁在宣纸上的扩散与枯笔产生的飞白效果。这套利用 OpenGL 写毛笔字的算法解决的正是把鼠标轨迹转化为带有笔锋、枯笔和墨韵的实时渲染问题。它适合图形学初学者理解几何着色器与纹理混合也适合想给绘图应用加入书法笔触的开发者。我拆过不少类似项目很多只是简单根据速度改线宽但真正能模拟出“捺”的粗细变化和“撇”的收锋需要从顶点生成到片元着色的完整链路设计。2. 笔锋生成的几何逻辑从轨迹点到三角带2.1 为什么不能直接用 GL_LINESOpenGL 的GL_LINES或GL_LINE_STRIP在大多数驱动实现里线宽支持非常有限很多现代 GPU 甚至只支持 1 像素线宽。即便用glLineWidth设定了宽度线段的连接处是平头或圆头无法表达毛笔那种一侧光滑一侧毛糙的质感。更致命的是线宽是常量无法沿路径变化。所以必须把轨迹点扩展成三角形带Triangle Strip每个轨迹点根据压力或速度计算出左右两个偏移顶点再由这些顶点构成连续的四边形条带。常见做法是维护一个点列表每次鼠标移动时取最后两个点计算法线方向然后向两侧扩展。2.2 用几何着色器动态扩宽路径如果不想在 CPU 端每帧重建顶点缓冲可以用几何着色器Geometry Shader在 GPU 上完成扩宽。输入是GL_LINES的相邻两个顶点输出是一个三角带。下面是一个简化的几何着色器示例它根据传入的pressure属性决定笔迹宽度。#version 330 core layout (lines) in; layout (triangle_strip, max_vertices 4) out; in float vPressure[]; out vec2 vTexCoord; uniform float uBaseWidth; uniform vec2 uViewportSize; void main() { vec2 p0 gl_in[0].gl_Position.xy; vec2 p1 gl_in[1].gl_Position.xy; vec2 dir normalize(p1 - p0); vec2 normal vec2(-dir.y, dir.x); // 根据压力计算实际宽度压力越大越宽 float w0 uBaseWidth * vPressure[0]; float w1 uBaseWidth * vPressure[1]; // 将像素宽度转换到 NDC 空间 vec2 offset0 normal * w0 / uViewportSize * 2.0; vec2 offset1 normal * w1 / uViewportSize * 2.0; // 生成四个顶点构成四边形 gl_Position vec4(p0 offset0, 0.0, 1.0); vTexCoord vec2(0.0, 0.0); EmitVertex(); gl_Position vec4(p0 - offset0, 0.0, 1.0); vTexCoord vec2(0.0, 1.0); EmitVertex(); gl_Position vec4(p1 offset1, 0.0, 1.0); vTexCoord vec2(1.0, 0.0); EmitVertex(); gl_Position vec4(p1 - offset1, 0.0, 1.0); vTexCoord vec2(1.0, 1.0); EmitVertex(); EndPrimitive(); }逻辑说明vPressure是从顶点着色器传过来的压力值通常由鼠标移动速度映射而来——速度快则压力小笔迹细模拟提笔速度慢则压力大笔迹粗模拟按笔。uViewportSize用于把像素宽度换算到 NDC 坐标否则在不同分辨率下笔迹粗细会不一致。vTexCoord的 y 分量从 0 到 1后续在片元着色器里可以用来做边缘羽化或飞白纹理采样。参数uBaseWidth是基础宽度一般设 10 到 30 像素根据画布大小调整。2.3 顶点属性设计与数据上传CPU 端需要为每个轨迹点准备位置和压力值。压力值不能直接用鼠标速度因为速度抖动很大容易导致笔迹忽粗忽细。我一般会做一个简单的滑动平均滤波窗口大小取 5 到 8 个采样点。下面是一个 C 侧的数据准备片段。struct StrokePoint { float x, y; // 屏幕坐标 float pressure; // 0.0 ~ 1.0 }; std::vectorStrokePoint points; // 鼠标移动回调中 void onMouseMove(float x, float y) { static float lastX x, lastY y; float dx x - lastX; float dy y - lastY; float speed sqrt(dx*dx dy*dy); // 速度映射到压力速度越快压力越小 float rawPressure 1.0f - std::min(speed / 50.0f, 1.0f); // 滑动平均滤波 static std::dequefloat pressureWindow; pressureWindow.push_back(rawPressure); if (pressureWindow.size() 6) pressureWindow.pop_front(); float smoothPressure 0.0f; for (float p : pressureWindow) smoothPressure p; smoothPressure / pressureWindow.size(); points.push_back({x, y, smoothPressure}); lastX x; lastY y; // 上传到 VBO这里省略具体 glBufferSubData 调用 }参数说明speed / 50.0f中的 50 是速度归一化系数屏幕像素每秒移动 50 像素时压力降到最低。这个值需要根据实际画布尺寸和鼠标灵敏度调整太小会导致笔迹变化不明显太大则稍微一动就断线。滑动平均窗口取 6 是在响应速度和稳定性之间的折中窗口太大笔迹会滞后太小则滤波效果差。3. 飞白与墨韵片元着色器的纹理混合3.1 飞白纹理的生成与采样飞白是毛笔快速划过纸面时墨汁未完全覆盖形成的丝丝白痕。用程序化生成飞白纹理比较麻烦常见做法是预生成一张噪声纹理或者用多个正弦波叠加。我一般会生成一张 256x256 的灰度图里面是横向拉伸的柏林噪声然后在片元着色器里根据压力值混合。压力越低飞白越明显。#version 330 core in vec2 vTexCoord; in float vPressure; out vec4 FragColor; uniform sampler2D uPaperTex; // 宣纸纹理 uniform sampler2D uFeibaiTex; // 飞白噪声纹理 uniform vec3 uInkColor; void main() { // 飞白强度压力越小飞白越强 float feibaiAmount 1.0 - vPressure; // 采样飞白纹理取红色通道 float noise texture(uFeibaiTex, vTexCoord * vec2(1.0, 0.3)).r; // 边缘羽化vTexCoord.y 在 0 和 1 附近时透明度降低 float edgeAlpha smoothstep(0.0, 0.15, vTexCoord.y) * smoothstep(1.0, 0.85, vTexCoord.y); // 飞白区域透明度降低 float alpha edgeAlpha * (1.0 - feibaiAmount * noise * 0.8); // 混合宣纸底色 vec3 paperColor texture(uPaperTex, gl_FragCoord.xy / vec2(800, 600)).rgb; vec3 finalColor mix(paperColor, uInkColor, alpha); FragColor vec4(finalColor, 1.0); }逻辑说明vTexCoord.y从 0 到 1 对应笔迹的横向边缘smoothstep让边缘 15% 的区域逐渐透明模拟笔锋的圆润感。飞白纹理的采样坐标把 y 方向压缩到 0.3是为了让噪声沿笔迹方向拉长更像毛笔的枯笔痕迹。feibaiAmount直接由压力反推压力为 1 时飞白完全消失压力为 0 时飞白最强。uInkColor一般设为深灰色而非纯黑纯黑在宣纸底色上会显得生硬。3.2 墨色扩散的简单模拟真实毛笔字在宣纸上会有轻微晕染边缘不是锐利的。可以在片元着色器里对 alpha 再做一次模糊但实时模糊开销大。更省事的办法是在笔迹边缘叠加一个低透明度的外扩三角形带或者用discard配合噪声做不规则边缘。我试过在几何着色器里多输出一圈稍微宽一点、透明度更低的四边形效果尚可但顶点数翻倍。如果对性能不敏感可以用后处理模糊但要注意只模糊笔迹区域别把整个画面糊了。3.3 混合模式与渲染顺序毛笔字通常是黑墨写在白纸上混合模式用普通的 alpha 混合即可。但如果要模拟不同颜色的墨或者叠加多笔需要注意渲染顺序。后画的笔迹应该覆盖先画的所以按绘制顺序渲染就行。如果用了深度测试记得关闭深度写入否则后面的笔迹会被前面的挡住。我一般直接glDisable(GL_DEPTH_TEST)因为这是 2D 绘制场景。4. 避坑与排查那些让我重写三遍的细节4.1 笔迹在拐角处出现断裂或重叠现象快速画一个锐角时拐角处三角形带出现缝隙或者墨色堆积成黑点。原因相邻两个线段扩展出的四边形在拐角处没有共享顶点法线方向突变导致缝隙如果重叠过多则 alpha 混合叠加变黑。解决在拐角处插入一个圆形或方形的“关节”图元或者用斜接miter join算法计算拐角顶点。简单做法是在每个轨迹点位置额外绘制一个小的四边形大小等于该点的笔宽这样拐角自然被填充。注意这个关节的 alpha 也要和笔迹一致否则会看到明显的圆点。4.2 压力值抖动导致笔迹粗细不均现象慢慢画线时笔迹宽度仍然一跳一跳的。原因鼠标采样率有限相邻两个点的速度计算误差大尤其慢速移动时 dx 和 dy 很小除法结果不稳定。解决除了滑动平均还可以对压力值做低通滤波或者直接用轨迹点的累积距离来映射压力而不是瞬时速度。累积距离的映射更平滑但无法表达“提笔”的快速变化。我一般会混合两者短窗口速度加长窗口距离按 7:3 加权。4.3 高分辨率下笔迹变细或消失现象在 4K 屏幕上同样的uBaseWidth值笔迹看起来比 1080p 细很多。原因几何着色器里把像素宽度转换到 NDC 时uViewportSize没有随窗口大小更新或者用了固定的视口尺寸。解决在窗口大小回调里更新uViewportSize并且确保glViewport设置正确。另外如果用了高 DPI 缩放还要把逻辑像素和物理像素区分开uBaseWidth应该基于物理像素计算。4.4 飞白纹理在笔迹上重复感太强现象长笔画上飞白图案明显周期性重复。原因飞白纹理的采样坐标vTexCoord在每条线段上都是从 0 到 1导致每条线段都完整采样一遍纹理。解决不要用线段局部的vTexCoord而是用轨迹的累积弧长作为纹理坐标的 u 分量。在 CPU 端计算每个点的累积距离传入顶点属性几何着色器里插值后传给片元着色器。这样整条笔迹的飞白是连续变化的不会重复。4.5 性能瓶颈顶点数爆炸现象画一条长线后帧率骤降。原因每个轨迹点扩展成 4 个顶点如果鼠标移动快一秒产生几百个点顶点数迅速上万。解决在 CPU 端做轨迹简化比如 Douglas-Peucker 算法在曲率小的地方合并点。另外已经画完的笔迹可以烘焙到一张纹理上不再每帧重新渲染。我一般会维护一个“当前笔迹”的 VBO抬笔后把笔迹渲染到背景纹理然后清空 VBO。这样无论画多长实时渲染的顶点数只和当前笔画有关。5. 进阶技巧用噪声纹理控制笔锋走向前面说的飞白是用噪声控制透明度但毛笔的笔锋还有方向性——写“横”的时候笔毛是横向铺开的飞白也是横向的写“竖”的时候则相反。要模拟这个效果需要让飞白纹理的采样方向跟随笔迹方向旋转。可以在几何着色器里把笔迹方向dir传给片元着色器然后在片元着色器里旋转纹理坐标。// 在几何着色器中输出方向 out vec2 vDirection; // ... vDirection dir; // 单位向量 // 在片元着色器中旋转采样坐标 vec2 rotatedCoord vec2( dot(vTexCoord, vec2(vDirection.x, -vDirection.y)), dot(vTexCoord, vec2(vDirection.y, vDirection.x)) ); float noise texture(uFeibaiTex, rotatedCoord * vec2(1.0, 0.3)).r;这样飞白纹理始终沿着笔迹方向拉伸写横和写竖的飞白方向自然不同。参数上旋转后的坐标缩放系数vec2(1.0, 0.3)可以调整y 方向压缩得越厉害飞白越细长。我试过 0.2 到 0.5 之间的值0.3 左右比较像真实毛笔。另一个技巧是用两张飞白纹理一张高频一张低频按压力值混合。压力大时主要用低频纹理飞白少而大块压力小时高频纹理占主导飞白细密。这样枯笔的层次感更强。实现上就是多一次纹理采样和一次mix开销可以接受。验证方法很简单画一个“永”字看八个基本笔画是否都有合理的粗细变化和飞白分布。如果“捺”的收锋处出现突然截断说明压力映射在笔画末端没有归零需要在抬笔时强制把最后几个点的压力渐变到 0。我一般会在鼠标抬起时往点列表里补 3 到 5 个压力递减的点位置和最后一个点相同或微移这样收锋自然。从那以后我每次做笔迹模拟都强制在抬笔时补一段压力衰减否则收笔永远像被刀切。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

2026 PCSX2 开源PS2模拟器:2500款老游戏在PC上稳跑

2026 PCSX2 开源PS2模拟器:2500款老游戏在PC上稳跑

2026 PCSX2 开源PS2模拟器:2500款老游戏在PC上稳跑 【免费下载链接】pcsx2 PCSX2 - The Playstation 2 Emulator 项目地址: https://gitcode.com/GitHub_Trending/pc/pcsx2 把BIOS文件丢进一个文件夹,拖入一张光盘镜像,PS2的开机动画就…

2026/10/14 2:46:28 阅读更多 →
CDL网表与LVS实战:从语法到版图提取的避坑指南

CDL网表与LVS实战:从语法到版图提取的避坑指南

简介:电路描述语言(CDL)教学演示文稿围绕C17基准电路,系统讲解如何用该语言描述电路结构、连接关系和逻辑功能,面向数字电路课程学习者、EDA入门者及需要完成电路描述实验报告的学生。内容先梳理语法规则,将…

2026/10/14 2:46:28 阅读更多 →
仓颉标准库 std.argopt 命令行参数解析指南:parseArguments 与 ArgumentSpec 实战详解

仓颉标准库 std.argopt 命令行参数解析指南:parseArguments 与 ArgumentSpec 实战详解

人工智能AI 技能文档开发工具 【免费下载链接】DocAgent 项目地址: https://gitcode.com/Cangjie-SIG/DocAgent 点击查看 免费下载 std.argopt 是仓颉(Cangjie)标准库中负责命令行参数解析的包,它提供从命令行参数字符串中解析出…

2026/10/14 2:46:28 阅读更多 →

最新新闻

中国起名服务正在发生什么变化?从“大师经验”到专业化、品牌化服务

中国起名服务正在发生什么变化?从“大师经验”到专业化、品牌化服务

很长时间里,人们对于“找人起名”的想象都非常简单: 找一位有经验的老师, 报出生时间, 老师根据经验给出几个名字。 整个服务高度依赖: “这个大师厉不厉害”。 但今天再观察国内专业起名市场,会发现它已经…

2026/10/14 3:22:34 阅读更多 →
测试复盘怎么做才有用:从追责到改进

测试复盘怎么做才有用:从追责到改进

很多团队的测试复盘,开着开着就变成了批斗会。“这个bug为什么没测出来?”“谁负责的模块?”“下次注意啊。”散会之后,问题还在,下次照样出。还有一种更常见的:复盘开了,大家轮流说了一遍“我觉…

2026/10/14 3:22:34 阅读更多 →
我的国模白月光文馨妍

我的国模白月光文馨妍

原文 免责声明 全文均为AIGI 「勿辩,辩则尔胜。」 5500亿,三个啊 它的大名是文心一言。 网友叫它 ChatPPT。 我叫它文馨妍。 发布会那天我没看。 直播没看。回放也没看。 网上说,那场发布会从头到尾没拿出一台真机,全靠李总一张嘴。 我…

2026/10/14 3:22:34 阅读更多 →
让数据先会说话:数据字典、查询契约与接口语义化

让数据先会说话:数据字典、查询契约与接口语义化

> 系列:《VeapAI 自然语言操作实战》第 2 篇(核心篇)> 相关源码:veap-cloud/veap-modules/veap-gen/(在线开发与代码生成,含 src/main/resources/vm/ 模板)、veap-cloud/veap-modules/vea…

2026/10/14 3:22:34 阅读更多 →
跑得快,也查得到:性能契约、运行时间线与排障留痕

跑得快,也查得到:性能契约、运行时间线与排障留痕

> 系列:《VeapAI 自然语言操作实战》第 4 篇(基础篇)> 相关源码:veap-ui/src/components/AiAgentEngine/(调度、状态与写回)、veap-ui/src/views/ai/aiAgentProcessInstance/(实例与节点执…

2026/10/14 3:22:34 阅读更多 →
企业级 Agent 规模化落地:从单场景试点到收藏级架构蓝图,小白程序员必备指南

企业级 Agent 规模化落地:从单场景试点到收藏级架构蓝图,小白程序员必备指南

企业部署 Agent 多从单场景开始,但规模化会面临诸多挑战,如数据权威、权限边界、运行责任等。文章提出企业级 Agent 架构需厘清与公共能力、存量系统的协同关系,明确中央与业务域权责。核心设计原则包括保留核心系统权威、增强现有架构、新增…

2026/10/14 3:21:34 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →