MATLAB 贝塞尔曲面 3D 建模学习记录

从连 surf() 为什么要转置都搞不明白,到用贝塞尔曲面建模出豌豆射手、向日葵和僵尸的完整动画场景——这篇文章记录了整个学习过程。如果你也在学MATLAB的表面建模,希望这里面的踩坑记录能帮到你。

260314

不知道写什么了,写几个字凑凑数吧,不然显得太敷衍了

260315

先看看matlab能干什么吧,问了下grok,叫他生成一个篮球,就生成下面这个了:

image-20260315205604304

我觉得这种体现不了画图的过程,就让他画一个有网格的篮球,我不需要上色,下面这是效果:

image-20260315205521643

ai写的代码我没看,不过感觉大部分逻辑都是比较简单的,所以代码就会很长,不过用ai来建一些简单的物体还是可行的。

让ai之间互相评估打架,在这个项目里面应该会有很大的作用,我是这样感觉的哈。

260316

用matlab来画图最大的感受是:他是用细小间隔的分段点来近似一个连续的区间的,你分段分的越小,自然图像就看起来越连续,就看不出那些网格了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
clear;clc;close all;
un=100; wn=4;
u=linspace(0,1,un)'; w=linspace(0,1,wn)';
Sx=zeros([un,wn]); Sy=zeros([un,wn]); Sz=zeros([un,wn]);
for i=1:un
for j=1:wn
Pux=sin(u(i)*2*pi+pi/2); Qux=2*sin(u(i)*2*pi);
Sx(i,j)=(1-w(j))*Pux+w(j)*Qux;
Puy=cos(u(i)*2*pi+pi/2); Quy=2*cos(u(i)*2*pi);
Sy(i,j)=(1-w(j))*Puy+w(j)*Quy;
Puz=0; Quz=1;
Sz(i,j)=(1-w(j))*Puz+w(j)*Quz;
end
end
subplot(131);
surf(u,w,Sx','edgecolor','k','facealpha',0.5); xlabel('u');ylabel('w');
subplot(132);
surf(Sx,Sy,Sz,'edgecolor','k','facealpha',0.5); xlabel('x');ylabel('y');
subplot(133);
surf(Sx',Sy',Sz','edgecolor','k','facealpha',0.5); xlabel('x');ylabel('y');

我有一个很大的问题,就是surf()里面的Sx,Sy,Sz我发现转置和不转置画出来的图像是没有什么区别的,那我为什么还要进行转置啊???

其实就是没有什么区别(有区别就怪了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
假设我们有下面三个 3 × 3 矩阵(为了简单,用很小的网格):
X = [ 0 1 2
0 1 2
0 1 2 ]

Y = [ 0 0 0
1 1 1
2 2 2 ]

Z = [ 0 1 0
1 3 1
0 1 0 ]
你就可以理解为有九个点的坐标告诉你了:
对应的点坐标:
(X(1,1),Y(1,1),Z(1,1)) = (0,0,0)
(X(1,2),Y(1,2),Z(1,2)) = (1,0,1)
(X(1,3),Y(1,3),Z(1,3)) = (2,0,0)
……
(X(3,3),Y(3,3),Z(3,3)) = (2,2,0)
即使你转置了其实也还是这些点在进行连线并且填充这些小方格

image-20260316200108341

还有,为什么surf(u,w,Sx’,‘edgecolor’,‘k’,‘facealpha’,0.5); xlabel(‘u’);ylabel(‘w’)里面Sx非得转置不可,不转置就会报错呢?

因为surf指令有一个原则:

  1. X和Y是向量,Z是矩阵,要满足:
  • length(X) == size(Z, 2) (X 的长度 = Z 的列数)

  • length(Y) == size(Z, 1) (Y 的长度 = Z 的行数)

    至于为什么是这样的,哎,其实我不懂啊…

    10X1 7X1 7X10是对的

    10X1 7X1 10X7是错的

  1. surf(X, Y, Z) 三个都是矩阵,且尺寸相同

    解释见上面(嘿嘿偷个懒偷个懒):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
**Bezier曲面的Matlab实现**
clear;clc;close all;
m=3; pxx=[0 2 4 6]; pyy=[6 4 2 0];
[px,py]=meshgrid(pxx,pyy);
pz=[0 1 1 0;
0.5 1.5 1.5 0.5;
0.5 1.5 1.5 0.5;
0 1 1 0];
px([1,4],1)=0;py([1,4],1)=3;pz([1,4],1)=0;
%-----绘制控制点----------
figure(1)
plot3(reshape(px,[(m+1)^2,1]),reshape(py,[(m+1)^2,1]),...
reshape(pz,[(m+1)^2,1]),'rs','markerfacecolor','r');
hold on;
mesh(px,py,pz,'linewidth',1.5,'edgecolor','g','facealpha',0);
%----绘制Bezier曲面------
u=linspace(0,1,30)';
w=linspace(0,1,30)';
Bu=zeros([30,m+1]);
Bw=zeros([30,m+1]);
for i=0:m
Bu(:,i+1)=prod(i+1:m)/prod(1:m-i)*u.^i.*(1-u).^(m-i);
end
for i=0:m
Bw(:,i+1)=prod(i+1:m)/prod(1:m-i)*w.^i.*(1-w).^(m-i);
end
Sx=Bu*px*Bw'; Sy=Bu*py*Bw'; Sz=Bu*pz*Bw';
surf(Sx,Sy,Sz,'linestyle','-');colormap("sky");

看不动了看不动了,先贴着,留着明天进步吧。

260319

之所以感觉有点难搞是因为你没有练习,可以试着建立一下小鱼的简单表面建模,采用三次贝塞尔曲面进行拼接练习一下

image-20260319195537559

260324

书接上回260319的曲面绘制代码,我们来一行一行细说

1
clear; clc; close all;

这“三件套”必不可少,因为我们要清除之前的变量赋值、清空命令窗口,关闭打开的图形窗口,养成这个习惯只有好处没有坏处

1
m = 3;n = 3;

干什么:设置了贝塞尔曲面的阶数为3(有个问题啊,mn是可以不同的吗?)

数学意义:说明有(m+1)x(n+1)个控制点,也就是(3+1)x(3+1)=16个控制点,阶数越高,曲面越灵活但计算量越大

启发:如果发现曲面很复杂无法通过三次贝塞尔曲面来进行绘制,我们可以采用四阶甚至五阶贝塞尔曲面来绘制

1
2
3
pxx = [0 2 4 6];
pyy = [6 4 2 0];
[px, py] = meshgrid(pxx, pyy);
image-20260324210508024

image-20260324210916564

image-20260324210934976

干什么:pxx、pyy定义 x 方向和 y 方向控制点的坐标序列,使用meshgrid生成一个4X4矩阵,现在 px(i,j) 和 py(i,j) 就是第 i 行、第 j 列控制点的 (x,y) 坐标了,就是下面这十六个点:

image-20260324211545730

有什么用:这是生成参数曲面控制点网格的标准写法,以后要自己建模时是会用到的

思考:标准写法?我一定是0246,不能是0123吗?

1
2
3
4
pz = [0 1 1 0;
0.5 1.5 1.5 0.5;
0.5 1.5 1.5 0.5;
0 1 1 0];

干什么:手动定义 4×4 的 z 坐标矩阵,给每个控制点一个高度

image-20260324212907726

有什么用:其实我们已经可以推测出大概的形状了,就是16个控制点的坐标已经确定了,Bézier 曲面的 z 值由控制点决定,这就是你“设计”曲面形状的地方

思考:那这样的话控制点的xy都是固定的了,但是我们实际的大作业控制点肯定不能是xy固定的啊,所以我是不是可以理解为:我们的pxpy都是要手动去输入的?

1
2
3
4
px(4,1)=3;py(4,1)=3;pz(4,1)=0;
px(4,4)=3;py(4,4)=3;pz(4,4)=0;
px(1,1)=3;py(1,1)=3;pz(1,1)=0;
px(1,4)=3;py(1,4)=3;pz(1,4)=0;

干什么:这一步很好的回答了你的问题,这就是更改控制点的坐标的,所以,你可以自己任意去定义控制点啦,想想就有无限的可能性

image-20260324213057099

有什么用:用处大了去了,我们可以调整边界点,对控制点可以说是随便定义了

思考:大作业里应该会经常这么干!两个不同的曲面进行拼接,就一定少不了控制点的重定义,因为要让控制点进行重合!所以在做项目之前就应该先想好控制点的位置,后续再微调,一旦项目成形了改点很难啊

1
figure(1)

干什么:打开第 1 个图形窗口(后面所有图都画在这里)

1
plot3(reshape(px,[numel(px),1]),reshape(py,[numel(py),1]),reshape(pz,[numel(pz),1]),'rs','markerfacecolor','r');

干什么:把 4×4 矩阵展平成 16×1 的列向量,用 plot3 画出 16 个红色实心方块(控制点),‘rs’ = red square,‘markerfacecolor’,‘r’ 让方块内部也填充红色

px、py被扁成这样了

image-20260324214951658image-20260324215228261

有什么用:把控制点的坐标显示出来了,可视化设计,可以显得自己的设计更加装逼一点,这是真的哈哈哈哈

思考:控制点的颜色代码都是什么?常见颜色的代码是啥样的?

image-20260324221729564

还有,我想用圆形来表示控制点呢?六边形呢?

为什么要进行这个操作呢?不扁可以吗?

1
hold on;

干什么:告诉 MATLAB“不要清空画布,继续在上面叠加图形”

1
mesh(px,py,pz,'edgecolor','b','facealpha',0);

干什么:用蓝色线条画出控制点之间的连线网格(像一张“网”)

注意:‘facealpha’,0 让网格面完全透明,只显示边线,把他改成0.5,你会发现一个新宇宙哈哈哈

1
[Sx,Sy,Sz] = PSBezierMxN(px, py, pz, m, n);

干什么:调用你自己写的函数 PSBezierMxN,计算 Bézier 曲面上的所有点坐标
输入参数:

  • px, py, pz:4×4 的控制点坐标矩阵(你前面已经手动改过四个角了)

  • m=3, n=3:u 方向和 w 方向的阶数(都是三次)

输出:

  • Sx, Sy, Sz:计算好的曲面点坐标,每个是 200×200 的矩阵(因为函数里 Rm=Rn=200)

对大作业的意义:把“计算曲面”这部分封装成函数后,后面你想画多少个不同的 Bézier 曲面,只要改控制点再调用一次这个函数就行,非常干净

函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function [Sx,Sy,Sz,Rm,Rn]=PSBezierMxN(px,py,pz,m,n)
Rm=200; Rn=200;
u=linspace(0,1,Rm)';
w=linspace(0,1,Rn)';
Bu=zeros([Rm,m+1]);
Bw=zeros([Rn,n+1]);
for i=0:m
Bu(:,i+1)=prod(i+1:m)/prod(1:m-i)*u.^i.*(1-u).^(m-i);
end
for i=0:n
Bw(:,i+1)=prod(i+1:n)/prod(1:n-i)*w.^i.*(1-w).^(n-i);
end
Sx=Bu*px*Bw';
Sy=Bu*py*Bw';
Sz=Bu*pz*Bw';
end
1
surf(Sx, Sy, Sz, 'linestyle', 'none');

干什么:用 surf 命令把刚才计算出的 200×200 个点画成一个曲面,‘linestyle’,‘none’:表示不画网格线,只画曲面本身。这样曲面没有线条干扰(适合做大作业展示)

思考:如果想看到曲面上的网格线,可以把 ‘none’ 改成 ‘-’ 或 ‘:’

1
colormap(jet);

干什么:给曲面设置颜色映射方案

思考:我想要一种色系的颜色映射方案呢?每一种方案的效果图有没有呢?

1
light('Position',[-1 0 0], 'Style', 'local');

干什么:在三维空间中添加一盏“灯”,让曲面产生明暗效果(立体感)

思考:似乎可以通过光源的不断转换,来让大作业更有质感,模拟光照

1
box off;

干什么:把图形四周的“盒子”边框去掉(只剩下坐标轴)

思考:其实这一步我都不是很理解,去不去掉好像没啥区别,先放在这里吧

来吧,看看ai生成的好东西:

啤酒瓶:

bottle_cap

山:

mountain

降落伞:

Parachute

机翼:

plane_wing

钱包:

wallet

(这个钱包我是真的没绷住啊哈哈哈)

260326

今天学习一下曲面的拼接哈😄,基本思想如下:

image-20260326231103644

一个简单的曲面拼接实验,说实话其实并不会很难

c1continuity

有点晚了,好困啊,先睡了,明天再搞吧,感觉真的要搞豌豆射手的话,嘴那一部分可以使用旋转命令进行,身子那一部分可以使用扫掠命令去搞,叶片底部那里可以用三次贝塞尔曲面/孔斯曲面进行绘制,大概的思路就是这样的啦,再加一个向日葵🌻,射手可以搞寒冰射手、火焰射手,再搞个僵尸🧟‍♀️我觉得就可以了

260330

眼睛干涩的厉害,不想看屏幕…

第一个问题,就是我怎么把每一片面片连接在一起,难不成要把一大堆面片都放在一个脚本里面?还是每一个部分分开建面片,最后进行一个聚合呢?

看了一下,一个模型基本上都是在一个.m文件里面去实现的

第二个问题,我怎么去找我想要的控制点坐标呢?

第三个问题,我怎么去知道自己需要的面片是什么类型呢?贝塞尔三次曲面?孔斯曲面?扫掠曲面?旋转曲面?

我看基本上采用的应该是贝塞尔三次曲面

哇去,学姐他们写的代码真的是,长,实在是长,有很多函数,而且主函数也很长,九百多行…

image-20260330212406597

调用我自己编写的函数,只要放在同一个文件夹就可以

搞懂了一部分:

豌豆射手上半头

wandoushooter_head

豌豆射手下半头(更改z值为负 对称得到的)

wandoushooter_head_2

茎秆(不过怎么看怎么不对劲)其实挺对的,因为y的一格是0.005,其实很小,茎秆是很细的

wandoushooter_jinggan

不过我想着代码里面应该会有对称的部分,但是今天暂时没有找到,好奇怪啊,这茎秆不是只画了一半吗哈哈哈,明天看看,说不定就有想法了

这里贴一张建模的效果图哈

image-20260330224129976

260331

有几个问题,第一个,就是为什么上下豌豆的头会出现没有完全连上的情况

这一步,看好了,关键就在这一步,主要是如果不这样调整的话就会出现这个问题,缝合的曲面不光顺了,说明需要不断地调整才能做好

image-20260331160615685

image-20260331160714086

第二个,后脑勺面片如何去缝合的,形成了不仅仅是一个矩形面片而是一个椭圆形面片(有点超乎我的想象了)

好吧其实不是你想不出来,而是本来就这样,其实,本质还是一个椭圆形面片,只不过叠加起来你看不太出来这是两块罢了哈哈哈

image-20260331162129014

第三个,怪异的茎秆,感觉从代码来看完全就不是这个效果啊…

感觉可以粗一点,不然杆子实在是太细了

image-20260331181459089image-20260331181515487

ctrl+r注释

茎秆部分发现有点太长了,缩短一点

image-20260331183632549image-20260331183729477

发现叶子太贴近了,可以改的分散一点

image-20260331183956394

效果还是不错的,茎秆有粗度的变化,叶子显得分散

image-20260331185640796image-20260331185729424|416

创建了三个函数,分别是绕xyz轴旋转的函数,这样我们后续使用就可以直接调用了

image-20260331192432748

这两个名字一定是要一样的,不然识别不了函数的

image-20260331193116524

image-20260331205518406

笑死了,做出了一个舞娘豌豆射手,很风骚有没有

image-20260331220752588

由于坐标倒了,所以我们使用这个指令 flipud(B17_x(:,4)) 进行矩阵的翻转,问题得到解决,侧面面片成功获得(原来是扭在一起的)

image-20260331222318283

image-20260331222204007

260401

发现者这一部分曲面存在一定的干涉,后续可以优化一下

发现把划横线地方的flipud删除掉就好了,为啥呢?

image-20260401104000937

今天把后面的叶片耳朵给做了,还贴了一些好看的贴图,把茎秆优化了一下,感觉效果很不错的,很厉害!

wandoushooter_all_1

260402

今天打算做好豌豆射出来的球,并且把小僵尸的建模思考搞清楚

这里发现豌豆的嘴巴可以加厚一圈,这样豌豆射手会更加真实一点

发现豌豆怎么这么大,居然比射手的嘴巴还大,笑死我了

image-20260402184824678

感觉效果还可以哈

image-20260402185605236

今天问了下grok,项目是这样实现的

文件名 作用 详细说明
main.m 整个程序的主入口(最重要) 负责读取所有 .obj 模型、转换贝塞尔曲面、设置动画逻辑、调用 visualizePart 绘制僵尸和豌豆射手、控制动画循环
Peashooter_dynamic.m 豌豆射手动态摇头动画 用你之前写的 Bézier 曲面控制点生成豌豆射手头部 + 叶子 + 茎,并实现左右摇头动画
Peashooter_static.m 豌豆射手静态展示 只显示静态的豌豆射手(不摇头)
convertQuadToBezier.m 四边形转 Bézier 曲面 把僵尸模型的四边形面片转换为 4×4 控制点的 Bézier 曲面片(核心曲面重建函数)
convertTriToBezier.m 三角形转 Bézier 曲面 把僵尸模型的三角形面片转换为 Bézier 三角形曲面(10个控制点)
Bezier.m 计算 Bézier 曲面上的点 给定 4×4 控制点,用矩阵方式计算出实际曲面上的插值点(21×21网格)
discretizeBezier.m Bézier 曲面离散化(生成网格) 把 Bézier 控制点离散成可以画的顶点和四边形面片
discretizeBezierTri.m 三角形 Bézier 离散化 处理三角形 Bézier 曲面的离散
genEdgePoints.m / genBezierEdgePoints.m 生成边上的控制点 为相邻面片的共享边生成中间控制点,保证拼接处 G¹ 连续
genInnerPoints.m 生成曲面内部控制点 根据四个角点生成内部 2×2 控制点
readQuadObj.m 读取 .obj 文件 读取僵尸的顶点、面片(支持四边形和三角形)
RotateX.m / RotateY.m / RotateZ.m 旋转矩阵函数 提供绕 X/Y/Z 轴旋转的功能
Symmetry.m 轴对称函数 提供关于 X/Y/Z 轴的镜像翻转
create_and_move_ball.m 生成并移动豌豆球 画一个绿色球并让它沿 X 轴移动(发射动画)
hat_move.m / move_hat.m 帽子移动函数 用于帽子掉落或移动的动画(未在主程序中使用)
draw_all_zombies.m 批量绘制僵尸(未使用) 尝试用 batch 方式渲染多个僵尸(目前未启用)
身体.obj / 左手.obj / 右手.obj / 左腿.obj / 右腿.obj / 头.obj 僵尸模型文件 Blender 导出的僵尸各部位几何模型

发现僵尸的表面建模很是困难,所以选择改变方式来进行建模,探索到的一个方式是使用obj文件导入matlab,进行一系列的函数编写,读取僵尸的顶点、面片等,并把面片更改为贝塞尔曲面即可

当程序读到 f 10//1 1//4 7//3 8//2 这行时:

它把这行拆分成 4 个部分,提取出每个部分的第一个数字(顶点索引),得到 [10, 1, 7, 8],然后存入 quad_faces 矩阵,表示这是一个由第10、1、7、8号顶点组成的四边形。

这篇文章我觉得应该是有帮助的

image-20260402195034483

写了这个代码,可以提取obj里面的顶点数据和面片数据

image-20260402202020934

260405

不想搞表面建模项目,随便写一点东西吧那就,今天是清明节,感觉人的心也随着节日飘走了

这样问ai

1
我想要把一大堆的obj文件读取并在一个matlab图给出来,并且可以自己手动去调整模型的空间位置,如何去实现

植物大战僵尸一共几关_植物大战僵尸关卡介绍_3DM单机

但是僵尸是倒着的,所以我们用symmetry函数给他正过来

1
[vx, vy, vz] = Symmetry(vertices(:,1), vertices(:,2), vertices(:,3), 'xoy');

260407

完成了向日葵的脸部完整建模、并且贴上了好看的贴图,好啊,好啊!

image-20260408132927994

260408

今天把向日葵、豌豆射手的茎秆部分后半部分空缺填上了,还给豌豆射手增加了嘴部面片的加厚设计。除此之外,还把文件夹整理了一下,非常不错的

sunflower_static

Peashooter_static

怎么样,是不是挺清爽的啦,之后的工作就是运动部分的啦!加油加油!

image-20260408172811688

260409

会出现以下报错,如何解决

1
2
3
4
错误使用 matlab.graphics.primitive.Surface/set
对象无效或已删除。
出错 Peashooter_dynamic (第 521 行)
    set(h1, 'XData', P1xr, 'YData', P1yr, 'ZData', P1zr);

ai给出的解决方法是:把所有surface创建语句全部移动到 while true 循环外面(只执行一次)。但我寻思我本来就是在外面创建的surface啊

把豌豆射手的动态效果做出来了,很惊艳的效果!

260410

优化了一下豌豆射手的茎秆摆动,让看起来更加同步了一些

向日葵的花瓣更加密集,不会那么空虚啦

sunflower_static

添加了向日葵的运动

260413

同时,因为循环一直在跑,也无法正常进行句柄移动操作

把b p h xr一些变量重命名了一下,加上了前缀pea、sun,这样的话就可以在同一个面板下面画图了,这是做的最正确的一件事

250415

实在是锯齿看得我生理不适了

image-20260415140758002

似乎面片会更加光滑,而且更有立体感

image-20260415143423879

具体更改如下,在向日葵、豌豆射手后面僵尸绘制前面加入了这段代码,具体功能是为了去除网格线,并实现平滑着色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plant_surfaces = findobj(gca, 'Type', 'surface');

for i = 1:length(plant_surfaces)
h = plant_surfaces(i);
if ishandle(h)
set(h, ...
'EdgeColor', 'none', ... % 彻底去掉网格线
'FaceColor', 'interp', ... % 平滑着色
'FaceLighting', 'gouraud', ...
'SpecularStrength', 0.7, ...
'DiffuseStrength', 0.9, ...
'LineStyle', 'none');
end
end

修复了向日葵茎秆异常移动的情况

250416

移动了僵尸的位置到稍远处

增加了僵尸被击杀以后自动消失的逻辑

优化了僵尸的颜色

锯齿问题解决(通过僵尸面片的消失)


经验提炼

从MATLAB零基础到做出完整的植物大战僵尸动画场景,最大的收获不是技术本身,而是学会了「怎么学一门新技术」:

  1. 先跑起来,再理解为什么。 一开始不理解 surf() 为什么要转置,不理解贝塞尔曲面的数学原理——没关系,先把代码跑起来看看效果,有了直观感受之后再回头理解原理,效率高得多。

  2. “感觉难是因为你没练。” 看教程觉得简单,自己动手全是bug。表面建模没有捷径,就是反复调控制点、反复试参数、反复看效果。

  3. 变量命名前缀是我做的最正确的事。 pea_、sun_、zombie_ 这种命名空间隔离,让多个角色能在同一个figure里独立动画。这不只是MATLAB技巧,是通用的软件工程思维。

  4. 审美驱动技术学习。 “锯齿看得我生理不适”——正是这种对粗糙画面的不能忍受,驱动我去学了Gouraud光照、interp着色这些渲染优化技术。有时候,不舒服是最好的老师。

  5. 跨格式整合能力很重要。 OBJ文件导入、面片解析、贝塞尔转换——最终项目不只是MATLAB原生建模,还整合了外部3D模型。学会处理不同格式的数据,比单纯会画曲面更有价值。

  6. 允许自己有低谷。 “今天不想搞表面建模项目了,随便写点”——这种状态很正常。重要的是低谷之后还能回来继续干。

  7. 让AI互相评审。 “让AI们互评互掐”——用多个AI工具交叉验证代码和方案,比只信一个AI的输出靠谱得多。

如果这篇文章对你有帮助,请我喝杯咖啡吧 ☕