
1. 心形曲线的三种数学来源先弄清我们要画什么总有人拿着Matlab心形绘图的问题来问我尤其是情人节前后和每学期图形学课设开始的时候。很多人的第一反应是上网抄一段代码plot出来一个红彤彤的桃心就交差了但代码为什么这么写、公式从哪来基本说不上来。这篇整理一下我实际用过的、也是社区里最常见的几种Matlab心形绘图方案从二维到三维、从静态到动画都有每一步都配上可以直接运行验证的完整代码和参数说明方便刚接触Matlab的同学直接照着复现也方便想深入理解绘图原理的人知道每一行在干什么。先统一交代一件事所谓“心形曲线”在数学上并不是只有一种标准定义。我见过最多的有三类来源一类是参数方程构造的闭合曲线一类是隐式方程f(x, y)0 描述的点集还有一类是极坐标下的心形线Cardioid。这三类对应到 Matlab 里分别是 plot、fimplicit、polarplot或转为直角坐标再 plot它们的绘制逻辑、适用场景和细节坑都不一样。我这里先放下结论表后面逐个拆开讲。方法数学形式对应 Matlab 工具特点参数方程法x16sin³t, y13cost−5cos2t−2cos3t−cos4tplot / fill轮廓饱满最接近日常“爱心”图案隐函数法(x²y²−1)³x²y³fimplicit一行函数句柄出图适合微积分课堂演示极坐标法ra(1−cosθ)polarplot / plot经典心脏线数学味道浓和爱心略有差异三维曲面法隐式曲面方程isosurface patch立体感强配合光影效果最好看1.1 参数方程一条曲线如何“设计”出心形先看最常见的那组参数方程x(t) 16·sin³(t)y(t) 13·cos(t) − 5·cos(2t) − 2·cos(3t) − cos(4t)t ∈ [0, 2π]这组公式不是拍脑袋写的。它的本质是用一个绕着圆心走一圈的参数 t分别控制横坐标和纵坐标的变化从而在平面里勾勒出封闭轮廓。x 这一路用 sin³(t)保证了左右两侧在 tπ/2 和 t3π/2 附近出现明显的峰谷形成心形顶部那两个圆弧。y 这一路则是一组不同频率的余弦函数叠加有点类似傅里叶级数逼近的思路——用不同频率的谐波组合出一个“顶部圆润、底部收尖”的纵剖面。系数的具体数值决定了整个图形的比例和弧度。很多资料直接把这组方程丢给你却不说为什么不能随便把 16 和 13 换成别的数。我实测下来如果只做等比例缩放图形形状基本不变但如果单独改动其中一个系数比如把 16 改成 10心形顶部那两个“瓣”就会相对变窄整体比例失衡。所以在做个性化调整时尽量整体缩放不要单独动单项系数。这里还有一个新手高频翻车点在 Matlab 里写 sin(t).^3而不是 sin(t)^3。因为 t 是向量sin(t) 也还是向量向量做乘方必须加“点”否则 Matlab 会直接报“数组运算”相关的维度错误。后面讲到具体代码时我会再强调一遍。1.2 隐式方程与极坐标另外两条路线隐式方程 (x²y²−1)³ x²y³ 是另一种经典写法。它不告诉你 y 怎么由 x 算出来而是用一个“x 和 y 满足某种关系”的方程来定义点集。Matlab 处理这类方程时不会去解 yf(x)而是利用等值线算法在给定区域里追踪 f(x, y)0 的轨迹这就是 fimplicit 的底层逻辑。极坐标心形线 ra(1−cosθ) 则是数学分析教材里常见的心脏线Cardioid这个名字本身就有“心”的意思。它属于外摆线的一个特例——一个圆在另一个大小相同的圆外侧滚动时圆周上某一点的轨迹。用极坐标画起来非常简洁只要让半径 r 随角度 θ 变化即可。三种数学来源对应三种“画法哲学”参数方程适合想精确控制轮廓的场景隐式方程适合展示隐函数图像、微积分相关教学极坐标适合处理角度相关的物理或几何问题。接下来我就按这四条路线把实际可运行的代码和参数细节逐一展开。2. 方法一参数方程绘图Matlab里最稳的入门做法2.1 核心代码与逐行拆解先给出一段我实测过很多次、几乎不会出错的版本% 心形参数方程绘图 t linspace(0, 2*pi, 300); % 参数 t 取 300 个点 x 16 * sin(t).^3; % 横坐标 y 13*cos(t) - 5*cos(2*t) - 2*cos(3*t) - cos(4*t); % 纵坐标 figure; plot(x, y, r, LineWidth, 2.5); % 红色曲线线宽 2.5 axis equal; % 纵横比设为相等 grid on; title(Heart by Parametric Equation);这段代码里最容易被忽略的是linspace(0, 2*pi, 300)中的 300。它表示把 0 到 2π 均匀切成 300 个采样点。如果改成 30画出来的就不是一条光滑曲线而是一个 30 边形折线看起来像多边形爱心如果改成 3000图形会更顺滑但绘制负担也稍微变大。我自己的经验是 200 到 500 之间足够300 是视觉和性能都很平衡的值。plot 里我习惯显式指定 r 和 LineWidth。新版 Matlab 默认的配色是自动的不指定的话可能是蓝色或其他颜色达不到红心效果LineWidth 默认只有 0.5直线细得可怜视觉冲击力差很多。通常 2 到 3 的线宽在导出图片后依然清晰。如果想得到填充的爱心把 plot 换成 fill 即可figure; fill(x, y, [0.85 0.25 0.35], EdgeColor, none); axis equal;fill 的第一个和第二个参数同样是 x、y 向量第三个参数是填充色用 RGB 三元组表示。我这里选了偏柔和的中国红不会像纯红 [1 0 0] 那么刺眼。EdgeColor, none 会去掉轮廓线让填充区域干净利落。加上 alpha 还能实现半透明效果alpha(0.85);2.2 为什么必须坚持“点运算”和 axis equal先谈点运算。t 是一个 1×300 的行向量sin(t) 也是 1×300sin(t).^3 的作用是对向量里的每一个元素分别求三次方。如果写成 sin(t)^3Matlab 会尝试把向量当成矩阵做乘方报错信息通常是“错误使用 ^输入必须为标量”。这是新手最容易踩的坑之一本质上是把矩阵运算和数组运算搞混了。记住一句口诀对向量、矩阵做乘方或乘除法绝大多数时候都要带点运算。这里不是“点”越多越好比如 sin(t).^3 的“点”加在 ^ 之前而不是 sin 之前。再看 axis equal。Matlab 默认的坐标轴比例不是 1:1它会根据数据范围自动拉伸来填满 figure 窗口。对于心形这种对纵横比例极敏感的图形如果不加 axis equalx 和 y 的刻度间距不一致心形就会变成扁扁的“鸭蛋形”。我见过很多同学问“为什么我的爱心是椭圆的”十有八九就是漏了这一句。axis equal 的含义是让 x 轴和 y 轴上的单位长度一致绘图区域按数据真实比例呈现。还有一个经常被忽视的点坐标轴范围。参数方程中 x 的范围约在 ±16 之间y 的范围约在 ±17 之间。如果后面想叠加文字、图形或者做动画建议手动加上xlim([-20 20]); ylim([-20 20]);保证可见区域留有余量避免图形边缘被裁剪。3. 方法二隐函数fimplicit一行画出方程心3.1 从方程到图形fimplicit的使用逻辑隐函数法最大的优势是代码极短不需要手动算点把方程原封不动交给 Matlab 即可% 隐函数心形 f (x, y) (x.^2 y.^2 - 1).^3 - x.^2 .* y.^3; figure; fimplicit(f, [-1.5 1.5 -1.5 1.5], LineWidth, 2, Color, [0.85 0.2 0.3]); axis equal;先解释匿名函数 (x, y)。它定义了一个关于 x、y 的函数句柄fimplicit 需要的正是这种“给两个坐标返回一个值”的输入形式。方程 (x²y²−1)³ x²y³ 被改写成了 f(x, y) (x²y²−1)³ − x²y³也就是把右边的项挪到左边画出来的就是 f(x, y)0 的等值线。fimplicit 的第二个参数是绘图区间[−1.5 1.5 −1.5 1.5] 分别表示 x 和 y 的范围。这个区间不能给太小否则图形会被截断也不能给太大否则心形只占图中间一小块视觉上很空。我实测 ±1.5 是刚好能完整容纳这个经典心形的最小范围。很多人不知道的是fimplicit 并不是把所有点代入方程去验证是否严格等于 0而是采用等值线追踪算法先把区域划分成网格计算每个网格顶点处的函数值再用插值找到函数值为 0 的等值线。所以它画出的线条天然就是光滑的不需要你手动采样这也让它在“想快速看一个隐函数长什么样”的场景下非常顺手。3.2 深色背景、颜色与坐标轴细节隐函数画爱心我习惯把背景调成深色视觉冲击力更强figure(Color, [0.1 0.1 0.15]); fimplicit(f, [-1.5 1.5 -1.5 1.5], LineWidth, 3, Color, [1 0.3 0.4]); axis equal; set(gca, Color, [0.1 0.1 0.15], GridColor, [0.5 0.5 0.5]);这段的效果是深色底、亮红心很适合直接当壁纸或海报素材。set(gca) 里的 Color 是坐标区背景色GridColor 是网格线颜色避免网格线和背景融为一体。这里也要提醒fimplicit 是从 R2016b 开始引入的函数如果你还在用很老的版本比如 R2014a 或更早需要改用 ezplotezplot((x^2y^2-1)^3 - x^2*y^3, [-1.5 1.5 -1.5 1.5]);但 ezplot 的线条控制和显示效果都偏旧现在新版本里它几乎被 fimplicit 完全取代了所以新写的代码我还是建议直接 fimplicit。对于隐函数方程本身还有个小细节值得知道方程右侧的 x²y³ 在 y 为负时是负值这意味着曲线并非只分布在上半平面图像底部也会出现一个小尖角收敛点这是这类方程自带的特点。有些同学看到心形下方多出了一小段线条担心代码错了其实那是方程数学性质的正常表现不是渲染 bug。4. 方法三极坐标心形线换个坐标系看同一件事4.1 从极坐标方程到极坐标系绘图极坐标心形线的标准形式是 r a(1 − cos θ)。这个方程里 r 是极径θ 是极角a 控制图形整体大小。用 Matlab 的 polarplot 画起来非常直观a 3; theta linspace(0, 2*pi, 500); r a * (1 - cos(theta)); figure; polarplot(theta, r, r, LineWidth, 2);polarplot 会直接画出极坐标系下的曲线背景自带角度格线数学味道很浓。这里 theta 取 500 个点同样是为了让曲线光滑。a 取 3 只是我习惯的大小你取 1、2、5 都会得到相似形状只是整体缩放不同。要特别说明的是极坐标心形线Cardioid和我们日常看到的“爱心”还是有一点点区别的。Cardioid 的尖端朝右或朝左取决于用的是 1−cosθ 还是 1cosθ整体像一颗心脏侧影而参数方程那个心形是标准的“桃心”顶部有两个对称的圆弧。很多数学教材里说的“心形线”其实是 Cardioid如果你交作业或做展示时要先明确自己的目标是哪一种避免被老师或同学追问“你的心形怎么不对称”。4.2 极坐标转直角坐标更自由的组合方式polarplot 有一个限制它独占一个坐标区想在同一张图里叠加网格线、文字、其他曲线会有点别扭。我更喜欢把极坐标方程转成直角坐标后用 plot 绘制a 3; theta linspace(0, 2*pi, 500); r a * (1 - cos(theta)); x r .* cos(theta); % 注意这里是点乘 y r .* sin(theta); figure; plot(x, y, r, LineWidth, 2); axis equal; grid on;转换公式就是极坐标与直角坐标的基本关系x r cosθy r sinθ。这里的“点乘”又是关键r 和 cos(theta) 都是 1×500 的向量必须用 .* 做逐元素乘法否则维度错误会直接中断运行。axis equal 依然不能少——Cardioid 如果横纵比例不相等会变成被拉长的畸形图案。转换后的好处是你可以自由控制坐标范围、叠加其他图形、设置标题和图例甚至把它和参数方程的心形放在同一张图对比。我经常做的一件小事是给两张图加上不同透明度叠加一起看比较两种心形轮廓的差异。如果想给极坐标心形填充颜色同样用 fillfill(x, y, [0.9 0.3 0.4], EdgeColor, none);因为极坐标方程是逐角度生成的闭合曲线首尾正好相接满足 fill 的闭合填充条件。5. 方法四三维心形曲面isosurface与光影渲染5.1 三维心形的核心代码与等值面原理二维画腻了接下来是立体感最强的三维心形。三维心形最常见的隐式曲面方程为(x² 2.25y² z² − 1)³ x²z³ 0.1125·y²z³Matlab 中推荐的绘制方式是 isosurface patch 组合% 三维心形曲面 f (x, y, z) (x.^2 (9/4)*y.^2 z.^2 - 1).^3 - x.^2 .* z.^3 - (9/80)*y.^2 .* z.^3; n 120; [x, y, z] meshgrid(linspace(-1.5, 1.5, n)); v f(x, y, z); figure; p isosurface(x, y, z, v, 0); patch(p, FaceColor, [0.85 0.25 0.35], EdgeColor, none, FaceAlpha, 0.9); view(3); camlight(headlight); lighting gouraud; axis equal;这段代码的核心在 isosurface 的“等值面”概念。f(x, y, z) 是一个三维标量场isosurface 要做的是找出所有满足 f 0 的空间点并把它们连接成三角网格曲面。你可以把它想象成在空间中切出一张“函数值恰好为零”的薄膜。正是这种机制避开了“解方程得到 zf(x,y)”的困难直接用体数据提取出隐式曲面。patch 的作用是把 isosurface 返回的三角网格模型渲染出来。FaceColor 设置表面颜色EdgeColor, none 去除三角形网格的边线这样曲面看起来才是光滑的FaceAlpha, 0.9 设置了 90% 的不透明度稍微透一点层次感更好。view(3) 等价于设置一个三维视角。camlight(headlight) 在镜头位置添加光源lighting gouraud 选择 Gouraud 光照模型让曲面明暗渐变更自然。5.2 网格密度与内存的平衡一个比你想的更现实的坑三维心形代码里最值得调整的参数是 n。n120 意味着 meshgrid 在三个维度上各生成 120 个点总共有 120³ ≈ 172.8 万个格点。x、y、z、v 四个数组都是 double 类型每个数组约 13.8 MB合计约 55 MB。这在现代电脑上完全没问题但如果把 n 改成 250总数会变成 250³ ≈ 1562.5 万个格点四个数组加起来超过 500 MB很多低配机器就会开始卡顿甚至报内存不足。我实测过n100 时曲面已经足够平滑肉眼几乎看不出三角形网格痕迹n150 是出图质量比较稳的选择n200 以上只有在你打算把图放得特别大比如做海报时才值得考虑。所以我的建议是先 n80 快速验证代码有没有跑通再按需要逐步提高到 120 或 150。不要一上来就 n300把自己电脑跑成幻灯片。另外patch 出的三维图形默认背景是白色。如果想做出那种深邃的星空感或深色背景可以加两行set(gcf, Color, [0.05 0.05 0.1]); lighting phong;lighting phong 比 gouraud 高一级高光更锐利适合表面比较光滑的模型。但如果电脑性能一般phong 渲染可能比 gouraud 略慢建议出图时二选一即可。三维心形的坐标轴也该注意虽然方程本身会“隐形”约束点在空间中的位置但 patch 出来之后坐标轴范围如果没有设置合适视角会忽远忽近。稳妥做法是xlim([-1.5 1.5]); ylim([-1.5 1.5]); zlim([-1.5 1.5]);这样每次运行出来的都在同一视角范围内看起来更规整。6. 进阶玩法会跳动的心从二维缩放到三维呼吸6.1 二维心跳动画循环更新坐标数据心形图如果只是静态显示趣味性有限。动画版才是真正能让人眼前一亮的东西。二维心形动画的核心思路非常简单在循环里不断改变曲线坐标然后刷新画面。t linspace(0, 2*pi, 300); x0 16 * sin(t).^3; y0 13*cos(t) - 5*cos(2*t) - 2*cos(3*t) - cos(4*t); figure; h plot(x0, y0, r, LineWidth, 3); axis equal; xlim([-20 20]); ylim([-20 20]); grid on; % 心跳动画 for k 1:120 s 1 0.06 * sin(2*pi * k / 30); % 心跳缩放因子 set(h, XData, s * x0, YData, s * y0); drawnow; pause(0.03); end这个动画的“心跳感”来自缩放因子 s。sin(2π·k/30) 会让 s 在 0.94 到 1.06 之间周期性变化周期约 30 帧。结合每帧 pause 0.03 秒整个心跳周期约 0.9 秒比较接近真实心跳节奏。你也可以把 30 改小一点心跳会变快改大一点则更舒缓。在动画循环中有几个细节很重要。第一必须用 set 更新已有图形对象的 XData 和 YData而不是反复调用 plot。plot 每次都会创建新的图形对象循环 120 次就会出现 120 根线叠在一起又卡又乱。第二drawnow 强制 Matlab 把绘图队列里的内容推到屏幕上这是动画的关键没有 drawnow所有更新会堆积到循环结束后一次性显示看不到动画效果。第三pause 的数值控制帧率0.03 秒左右人眼会觉得很顺滑太小则动画过快反而显得急促。6.2 三维心跳动画与GIF导出三维心形同样可以做成“跳动”效果。因为 patch 图形对象保存着曲面网格的顶点坐标直接对顶点坐标做整体缩放即可f (x, y, z) (x.^2 (9/4)*y.^2 z.^2 - 1).^3 - x.^2 .* z.^3 - (9/80)*y.^2 .* z.^3; n 100; [x, y, z] meshgrid(linspace(-1.5, 1.5, n)); v f(x, y, z); figure; p patch(isosurface(x, y, z, v, 0), FaceColor, [0.85 0.25 0.35], ... EdgeColor, none, FaceAlpha, 0.9); view(3); camlight(headlight); lighting gouraud; axis equal; xlim([-1.8 1.8]); ylim([-1.8 1.8]); zlim([-1.8 1.8]); verts p.Vertices; % 保存原始顶点坐标 for k 1:120 s 1 0.07 * sin(2*pi * k / 40); % 缩放因子 p.Vertices verts * s; % 所有顶点整体缩放 drawnow; pause(0.04); endp.Vertices 是一个 N×3 的矩阵每一行代表一个顶点的 (x, y, z) 坐标。整体乘以 s 相当于以原点为中心缩放模型心形会一鼓一鼓地膨胀收缩效果很真实。注意缩放幅度不要太大0.05 到 0.08 之间比较自然太大就像气球快吹爆了。如果想把这个动画保存成 GIF 发出去可以在循环里加几行抓帧代码for k 1:120 s 1 0.07 * sin(2*pi * k / 40); p.Vertices verts * s; drawnow; frame getframe(gcf); [im, map] rgb2ind(frame.cdata, 256); if k 1 imwrite(im, map, heart.gif, gif, LoopCount, inf, DelayTime, 0.05); else imwrite(im, map, heart.gif, gif, WriteMode, append, DelayTime, 0.05); end endgetframe 抓取当前图形窗口内容rgb2ind 把真彩色图像索引化为 GIF 支持的格式imwrite 第一次写入时设置 LoopCount 为无限循环之后追加写入每一帧。这里要注意 GIF 文件会随着帧数增加而变大如果导出的文件太大可以减小帧数把 120 改成 60或减小 figure 窗口尺寸。7. 高频踩坑与我的实测心得7.1 数组运算的“点”与线条细节二维、三维代码里我最常帮人排查的问题几乎都集中在数组运算和坐标轴比例上。下面这张表基本覆盖了我遇到的高频错误报错或异常表现常见原因解决办法错误使用 ^输入必须为标量sin(t)^3 少了点运算改成 sin(t).^3矩阵维度必须一致r .* cos(theta) 写成了 r * cos(theta)改成 .*心形变成扁椭圆缺少 axis equal加上 axis equal曲线像多边形折线linspace 采样点数太少增加到 200 以上线条太细不显眼LineWidth 默认 0.5设为 2 或 3图形边缘被裁剪坐标轴范围太小手动设置 xlim、ylim关于线条颜色我多说一句不要执着于纯红 [1 0 0]。在白色背景下纯红线条的视觉感受偏“刺眼”我用 [0.85 0.25 0.35] 这种偏温柔的红色系会更耐看。三维曲面同理FaceColor 用 [0.85 0.25 0.35] 配合深色背景整体观感是高级的。7.2 内存、版本与兼容性老版本用户的应对方案三维心形对电脑内存的敏感度比很多人想象的高。n120 已经够用但在真正发布或展示之前建议先看一下自己机器的内存占用。任务管理器里如果 Matlab 进程内存占用接近 1GB不要再往上加 n 了。另外isosurface 生成的网格三角形数量通常很大patch 渲染时如果滚动旋转视角明显掉帧说明模型面数偏高可以下调 n 到 100 或者 90视觉差别微小流畅度提升明显。版本兼容性方面fimplicit 是 R2016b 引入的polarplot 是 R2016a 引入的isosurface 则是比较老的函数早期版本也能用。如果你在一台版本很旧的 Matlab 上跑遇到函数不存在的情况优先考虑升级到新版本而不是反过来改代码适配旧版本。毕竟从 R2016b 到现在这么多年函数接口已经稳定新版兼容旧代码的负担远小于旧版迁新语法。绘制完成后如果要输出图片我推荐用 exportgraphics 而不是老式的 saveasexportgraphics(gcf, heart.png, Resolution, 300);300 DPI 的分辨率足够印刷或投屏且导出内容严格按照当前图形窗口的比例不会出现边框缺失。7.3 我的几点实际使用体会这几个心形绘图方法我实际用下来的感受可以浓缩成三句话第一如果只是“想要一张好看的红心图片”参数方程 fill 是最短路径代码短、形状标准、性能最好。第二fimplicit 方法更适合教学场景因为它直接展示隐函数方程和曲线之间的联系学生能直观理解“等值线”到底是什么。第三三维心形最有表现力但一定要控制好网格密度否则不是电脑卡死就是渲染出来一堆锯齿。我经常遇到的一个场景是有人跑完二维心形之后觉得“不过瘾”然后直接上三维心形代码结果笔记本风扇狂转。我的建议是先跑 n80确认自己喜欢这个三维效果再逐步提高分辨率。这不仅适用于心形所有 isosurface 类的绘图任务都可以遵循这个“先粗后精”的策略。最后一个非常实用的技巧如果在 Windows 上用了高分辨率显示器Matlab 窗口里的图放大后线条会很细导出 PNG 前先把 figure 窗口最大化再用 exportgraphics 导出线条粗细比例会更协调。这个细节我试过很多次导出前后的观感差别相当明显。