前情提要一:
前一个文章里,扒谱机与AI斗智斗勇,却一直没法绘制出标准的正五边形组成的十二面体。
网传CAD画图都是先绘制一个五边形,然后对应旋转拼接,而剩下的教程基本都是素描的教程。
经过百般搜索,扒谱机我找到了一个网站,然后参考了一下代码——
从它的源代码中找到了准确的点坐标应该是——
vertices_dodeca = [
% 第一组顶点
1, 1, 1; % 1
1, 1, -1; % 2
1, -1, 1; % 3
1, -1, -1; % 4
-1, 1, 1; % 5
-1, 1, -1; % 6
-1, -1, 1; % 7
-1, -1, -1; % 8
% 第二组顶点
phi, 1/phi, 0; % 9
phi, -1/phi, 0; % 10
-phi, 1/phi, 0; % 11
-phi, -1/phi, 0; % 12
% 第三组顶点
1/phi, 0, phi; % 13
1/phi, 0, -phi; % 14
-1/phi, 0, phi; % 15
-1/phi, 0, -phi; % 16
% 第四组顶点
0, phi, 1/phi; % 17
0, phi, -1/phi; % 18
0, -phi, 1/phi; % 19
0, -phi, -1/phi % 20
];
经过艰苦卓绝的面对应关系的书写,我也终于画出来了……
附个代码:
% 清空工作区并关闭所有窗口
clear;
close all;
% 计算黄金比例
phi = (1 + sqrt(5)) / 2;
colors = lines(5);
% 1. 正四面体
vertices_tetra = [1 1 1; 1 -1 -1; -1 1 -1; -1 -1 1];
faces_tetra = [1 2 3; 1 2 4; 1 3 4; 2 3 4];
% 2. 正六面体(立方体)
vertices_cube = [-1 -1 -1; 1 -1 -1; 1 1 -1; -1 1 -1; -1 -1 1; 1 -1 1; 1 1 1; -1 1 1];
faces_cube = [1 2 3 4; 5 6 7 8; 1 2 6 5; 2 3 7 6; 3 4 8 7; 4 1 5 8];
% 3. 正八面体
vertices_octa = [1 0 0; -1 0 0; 0 1 0; 0 -1 0; 0 0 1; 0 0 -1];
faces_octa = [1 3 5; 1 5 4; 1 4 6; 1 6 3; 2 3 6; 2 6 4; 2 4 5; 2 5 3];
% 4. 正十二面体
vertices_dodeca = [
% 第一组顶点
1, 1, 1; % 1
1, 1, -1; % 2
1, -1, 1; % 3
1, -1, -1; % 4
-1, 1, 1; % 5
-1, 1, -1; % 6
-1, -1, 1; % 7
-1, -1, -1; % 8
% 第二组顶点
phi, 1/phi, 0; % 9
phi, -1/phi, 0; % 10
-phi, 1/phi, 0; % 11
-phi, -1/phi, 0; % 12
% 第三组顶点
1/phi, 0, phi; % 13
1/phi, 0, -phi; % 14
-1/phi, 0, phi; % 15
-1/phi, 0, -phi; % 16
% 第四组顶点
0, phi, 1/phi; % 17
0, phi, -1/phi; % 18
0, -phi, 1/phi; % 19
0, -phi, -1/phi % 20
];
faces_dodeca = [
5 11 12 7 15; 8 12 7 19 20; 4 20 8 16 14; 2 9 10 4 14;
6 18 2 14 16; 5 17 1 13 15; 11 12 8 16 6; 7 19 3 13 15;
4 10 3 19 20; 1 13 3 10 9; 1 9 2 18 17; 11 6 18 17 5;
];
% 5. 正二十面体
vertices_icosa =[
0 1 phi; 0 1 -phi; 0 -1 phi; 0 -1 -phi;
1 phi 0; 1 -phi 0; -1 phi 0; -1 -phi 0;
phi 0 1; -phi 0 1; phi 0 -1; -phi 0 -1
];
faces_icosa = [
1 3 9; 1 3 10; 1 10 7; 1 5 7; 1 5 9;
2 7 12; 2 5 7; 2 4 12; 2 4 11; 2 5 11;
3 6 8; 3 6 9; 3 8 10; 4 6 8; 4 6 11;
4 8 12; 5 9 11; 6 9 11; 7 10 12; 8 10 12;
];
% 存储多面体数据
polyhedra = {
{'正四面体', vertices_tetra, faces_tetra};
{'正六面体', vertices_cube, faces_cube};
{'正八面体', vertices_octa, faces_octa};
{'正十二面体', vertices_dodeca, faces_dodeca};
{'正二十面体', vertices_icosa, faces_icosa}
};
% 批量绘制(增强可视化)
figure('Position', [100 100 1200 800]);
for i = 1:5
subplot(2, 3, i);
data = polyhedra{i};
name = data{1};
v = data{2};
f = data{3};
% 绘制填充面 + 线框
patch('Vertices', v, 'Faces', f, ...
'FaceColor', colors(i,:), 'EdgeColor', 'k', 'FaceAlpha', 0.3);
hold on;
patch('Vertices', v, 'Faces', f, ...
'FaceColor', 'none', 'EdgeColor', 'k', 'LineWidth', 1.2);
% 标注顶点索引
for idx = 1:size(v, 1)
text(v(idx,1), v(idx,2), v(idx,3), num2str(idx), ...
'HorizontalAlignment', 'center', 'VerticalAlignment', 'bottom', 'FontSize', 8);
end
title(name, 'FontSize', 12);
axis equal;
axis off;
view(3);
light('Position', [2 2 2]);
lighting gouraud;
hold off;
end
现在绘制柏拉图多面体代码都已经齐全,是时候做波在里面传播的仿真动画了。
前情提要二:
【微实验】新型鬼畜,球面波在柏拉图多面体里疯跑太魔幻!-CSDN博客
这篇文章里,笔者仅仅实现了立方体的部分,接下来,是时候把这两个结合起来了。
另外,参考b站up主帆雨动画,还需要进行的优化就是——
波峰使用蓝色标记,波谷使用橙色标记。为了便于观察,统一为黑白色后,振动幅度大的地方颜色深,基本上没有振幅的颜色浅。
尝试写了一下代码,看看效果:

除此之外,我希望仿真中能够把所有振幅为零的点标记画出(emm结果ai给的方案是标出等振幅面,效果并不是很理想)。
于是设计了一下阈值分段,振幅超过10%最大振幅的就直接使用纯色显示,最后优化的效果是这样子的:
这个代码是可以显示波源的震动频率不断变化的,当然,只要两列波的频率不统一,就不能形成稳定的干涉图样,但是同样可以形成干涉图样。可以发现类似双曲线的零振幅干涉纹样会逐渐向频率低的波源靠拢。
另外,在这个代码里,还可以修改不同波的振幅。通过修改参数,你可以得到很好看的纹样:
代码如下:
% 两个波源的干涉仿真(固定颜色标尺)
% 波峰用蓝色标记,波谷用红色标记,中间区域颜色渐变
% 固定颜色标尺,确保相同振幅对应相同颜色
clear; clc; close all;
%% 参数设置
% 空间参数
L = 50; % 空间范围
N = 2000; % 网格点数
x = linspace(-L, L, N); % x坐标
y = linspace(-L, L, N); % y坐标
[X, Y] = meshgrid(x, y); % 创建网格
% 波源参数
A1 = 4.2; % 波源1振幅
A2 = 1.9; % 波源2振幅
f1_start = 0.33; % 波源1初始频率
f2 = 1; % 波源2频率
f1_end = 0.3304; % 波源1最终频率
k = 2; % 波数
% 波源位置
x1 = -5; y1 = 2; % 波源1位置
x2 = 21; y2 = -13.2; % 波源2位置
% 时间参数
t_total = 10; % 总时间
dt = 0.05; % 时间步长
t = 0:dt:t_total; % 时间向量
n_frames = length(t); % 帧数
%自动设置固定振幅范围(保持对称)
fixed_max = A1 + A2;
fixed_min = -fixed_max;
% 创建颜色映射(高振幅纯色+低振幅渐变)
cmap = zeros(256, 3);
% 定义基础颜色:高振幅纯色+过渡色
blue_peak = [0 0 1]; % 波峰纯色(超过10%振幅)
red_trough = [1 0 0]; % 波谷纯色(低于-10%振幅)
white_mid = [1 1 1]; % 中间参考色
% 振幅阈值:超过10%最高振幅的部分用纯色
threshold_ratio = 0.1; % 10%阈值
% 计算阈值对应的索引(256阶中,前128阶对应负振幅,后128阶对应正振幅)
high_amp_idx = round(128 * threshold_ratio); % 10%对应的索引(约102)
low_amp_idx = 128 - high_amp_idx; % 渐变区域的索引长度(约26)
% --------------------------
% 波峰区域(正振幅):后128阶
% --------------------------
% 1. 高振幅区域(超过10%):纯蓝色(索引128+high_amp_idx ~ 256)
for i = high_amp_idx:128
cmap(i+128, :) = blue_peak; % 直接赋值纯色
end
% 2. 低振幅区域(低于10%):从白色渐变到浅蓝(索引128+1 ~ 128+high_amp_idx-1)
for i = 1:(high_amp_idx-1)
ratio = i / (high_amp_idx-1); % 0~1渐变比例
cmap(i+128, :) = white_mid + (blue_peak - white_mid) * ratio;
end
% --------------------------
% 波谷区域(负振幅):前128阶
% --------------------------
% 1. 高振幅区域(低于-10%):纯红色(索引1 ~ low_amp_idx)
for i = 1:low_amp_idx
cmap(i, :) = red_trough; % 直接赋值纯色
end
% 2. 低振幅区域(高于-10%):从浅红渐变到白色(索引low_amp_idx+1 ~ 128)
for i = (low_amp_idx+1):128
ratio = (i - low_amp_idx) / (128 - low_amp_idx); % 0~1渐变比例
cmap(i, :) = red_trough + (white_mid - red_trough) * ratio;
end
%% 动画绘制
figure('Position', [300 0 800 800]);
title('两个波源的干涉图样 (蓝色:波峰, 红色:波谷)');
xlabel('x'); ylabel('y');
for i = 1:n_frames
% 逐渐改变波源1的频率
f1 = f1_start + (f1_end - f1_start) * (i / n_frames);
% 计算到两个波源的距离
r1 = sqrt((X - x1).^2 + (Y - y1).^2);
r2 = sqrt((X - x2).^2 + (Y - y2).^2);
% 避免除以零
r1(r1 < 1e-6) = 1e-6;
r2(r2 < 1e-6) = 1e-6;
% 计算两个波的相位
phi1 = k * r1 - 2 * pi * f1 * t(i);
phi2 = k * r2 - 2 * pi * f2 * t(i);
% 计算两个波的振幅 (球面波振幅随1/r衰减)
wave1 = A1 * cos(phi1) ./ r1;
wave2 = A2 * cos(phi2) ./ r2;
% 合成波 (干涉)
total_wave = wave1 + wave2;
% 使用固定范围进行归一化,确保颜色标尺一致
total_wave_clamped = max(min(total_wave, fixed_max), fixed_min); % 限制范围
total_wave_norm = (total_wave_clamped - fixed_min) / (fixed_max - fixed_min); % 归一化到[0,1]
% 绘制干涉图样
imagesc(x, y, total_wave_clamped);
colormap(cmap);
colorbar;
caxis([fixed_min fixed_max]); % 固定颜色轴范围
axis equal tight;
title(['两个波源的干涉图样 (f1 = ' num2str(f1, '%.2f') ', f2 = ' num2str(f2) ')']);
drawnow;
end
disp('仿真完成!');
除此之外,在三维仿真时,可以掏空波谷部分,这样就能够更加直观地显示,但感觉MATLAB实现起来比较有难度。
(挖个坑,挖个坑……)
正式仿真:
首先,为了让代码看起来简洁高效,对上面两个“前情提要”进行封装:
1. 保存柏拉图多面体基础信息
创建 save_plato_polyhedra.m
脚本并运行:
% 定义柏拉图多面体顶点、面及名称并保存为mat文件
phi = (1 + sqrt(5)) / 2;
% 正四面体
tetra = struct( ...
'name', '正四面体', ...
'vertices', [1 1 1; 1 -1 -1; -1 1 -1; -1 -1 1], ...
'faces', [1 2 3; 1 2 4; 1 3 4; 2 3 4] ...
);
% 正六面体(立方体)
cube = struct( ...
'name', '正六面体', ...
'vertices', [-1 -1 -1; 1 -1 -1; 1 1 -1; -1 1 -1; -1 -1 1; 1 -1 1; 1 1 1; -1 1 1], ...
'faces', [1 2 3 4; 5 6 7 8; 1 2 6 5; 2 3 7 6; 3 4 8 7; 4 1 5 8] ...
);
% 正八面体
octa = struct( ...
'name', '正八面体', ...
'vertices', [1 0 0; -1 0 0; 0 1 0; 0 -1 0; 0 0 1; 0 0 -1], ...
'faces', [1 3 5; 1 5 4; 1 4 6; 1 6 3; 2 3 6; 2 6 4; 2 4 5; 2 5 3] ...
);
% 正十二面体
dodeca = struct( ...
'name', '正十二面体', ...
'vertices', [ ...
1, 1, 1; 1, 1, -1; 1, -1, 1; 1, -1, -1; -1, 1, 1; -1, 1, -1; ...
-1, -1, 1; -1, -1, -1; phi, 1/phi, 0; phi, -1/phi, 0; -phi, 1/phi, 0; ...
-phi, -1/phi, 0; 1/phi, 0, phi; 1/phi, 0, -phi; -1/phi, 0, phi; ...
-1/phi, 0, -phi; 0, phi, 1/phi; 0, phi, -1/phi; 0, -phi, 1/phi; 0, -phi, -1/phi ...
], ...
'faces', [ ...
5 11 12 7 15; 8 12 7 19 20; 4 20 8 16 14; 2 9 10 4 14; ...
6 18 2 14 16; 5 17 1 13 15; 11 12 8 16 6; 7 19 3 13 15; ...
4 10 3 19 20; 1 13 3 10 9; 1 9 2 18 17; 11 6 18 17 5 ...
] ...
);
% 正二十面体
icosa = struct( ...
'name', '正二十面体', ...
'vertices', [ ...
0 1 phi; 0 1 -phi; 0 -1 phi; 0 -1 -phi; ...
1 phi 0; 1 -phi 0; -1 phi 0; -1 -phi 0; ...
phi 0 1; -phi 0 1; phi 0 -1; -phi 0 -1 ...
], ...
'faces', [ ...
1 3 9; 1 3 10; 1 10 7; 1 5 7; 1 5 9; ...
2 7 12; 2 5 7; 2 4 12; 2 4 11; 2 5 11; ...
3 6 8; 3 6 9; 3 8 10; 4 6 8; 4 6 11; ...
4 8 12; 5 9 11; 6 9 11; 7 10 12; 8 10 12 ...
] ...
);
% 整合为结构体数组并保存
plato_polyhedra = [tetra, cube, octa, dodeca, icosa];
save('plato_polyhedra.mat', 'plato_polyhedra');
disp('柏拉图多面体数据已成功保存!');
2. 保存颜色配置信息
创建 save_color_config.m
脚本并运行:
% 定义波干涉颜色映射及参数并保存为mat文件
% 基础颜色定义
blue_peak = [0 0 1]; % 波峰颜色(蓝色)
red_trough = [1 0 0]; % 波谷颜色(红色)
white_mid = [1 1 1]; % 中间过渡色(白色)
threshold_ratio = 0.1; % 纯色与渐变色阈值比例
% 生成颜色映射(256级)
cmap = zeros(256, 3);
high_amp_idx = round(128 * threshold_ratio); % 波峰纯色起始索引
low_amp_idx = 128 - high_amp_idx; % 波谷纯色结束索引
% 波峰区域(正振幅):后128级
for i = high_amp_idx:128
cmap(i+128, :) = blue_peak;
end
for i = 1:(high_amp_idx-1)
ratio = i / (high_amp_idx-1);
cmap(i+128, :) = white_mid + (blue_peak - white_mid) * ratio;
end
% 波谷区域(负振幅):前128级
for i = 1:low_amp_idx
cmap(i, :) = red_trough;
end
for i = (low_amp_idx+1):128
ratio = (i - low_amp_idx) / (128 - low_amp_idx);
cmap(i, :) = red_trough + (white_mid - red_trough) * ratio;
end
% 保存颜色配置
save('wave_color_config.mat', 'cmap', 'blue_peak', 'red_trough', 'threshold_ratio');
第二步:主仿真代码(加载数据并运行仿真)
创建 polyhedron_wave_simulation.m
脚本:
(注:这里是直接使用解析式求解的版本,只展现了柏拉图多面体表面上的振幅)
% 柏拉图多面体表面波源干涉仿真(固定频率版)
% 功能:在多面体表面绘制两固定频率波源的干涉纹样,波峰蓝/波谷红/中间渐变
clear; clc; close all;
%% 加载外部数据
load('plato_polyhedra.mat', 'plato_polyhedra'); % 加载多面体数据
load('wave_color_config.mat', 'cmap'); % 加载颜色配置
%% 核心参数设置
% 波源参数(频率固定)
A1 = 4.2; % 波源1振幅
A2 = 1.9; % 波源2振幅
f1 = 0.33; % 波源1固定频率(不再变化)
f2 = 1.0; % 波源2固定频率
k = 2; % 波数(2π/λ)
% 三维波源位置(多面体内部坐标)
source1 = [0.5, 0.2, -0.3]; % 波源1空间坐标
source2 = [-0.3, -0.4, 0.6]; % 波源2空间坐标
% 时间参数(确保每个多面体仿真10秒)
t_total = 10; % 单多面体仿真总时长(秒)
dt = 0.05; % 时间步长(减小步长提升流畅度)
t = 0:dt:t_total; % 时间序列
n_frames = length(t); % 总帧数
frame_delay = dt; % 每帧停留时间(控制播放速度)
% 振幅范围(对称固定,确保颜色映射稳定)
fixed_max = A1 + A2;
fixed_min = -fixed_max;
%% 逐个多面体进行仿真
for poly_idx = 1:length(plato_polyhedra)
% 获取当前多面体数据
poly = plato_polyhedra(poly_idx);
name = poly.name;
vertices = poly.vertices; % 体表顶点坐标(用于计算波振幅)
faces = poly.faces; % 多面体面定义
% 创建专属显示窗口
fig = figure('Position', [300 200 800 800]);
title([name ' 表面波干涉仿真(f1=' num2str(f1) ', f2=' num2str(f2) ')'], 'FontSize', 14);
% 预计算顶点到波源的距离(体表点到波源的空间距离)
r1 = sqrt(sum((vertices - source1).^2, 2)); % 顶点到波源1的距离
r2 = sqrt(sum((vertices - source2).^2, 2)); % 顶点到波源2的距离
r1(r1 < 1e-6) = 1e-6; % 避免距离为0导致除零
r2(r2 < 1e-6) = 1e-6;
% 动画循环(逐帧更新干涉纹样)
for frame = 1:n_frames
t_current = t(frame); % 当前时间点
% 计算两列波在体表顶点的振幅(球面波公式:A*cos(kr-ωt)/r)
phi1 = k * r1 - 2 * pi * f1 * t_current; % 波源1相位
phi2 = k * r2 - 2 * pi * f2 * t_current; % 波源2相位
wave1 = A1 * cos(phi1) ./ r1; % 波源1在体表的振幅
wave2 = A2 * cos(phi2) ./ r2; % 波源2在体表的振幅
total_wave = wave1 + wave2; % 干涉后总振幅
total_wave_clamped = max(min(total_wave, fixed_max), fixed_min); % 限制振幅范围
% 计算顶点颜色索引(映射到颜色表)
color_idx = round(((total_wave_clamped - fixed_min) / (fixed_max - fixed_min)) * 255) + 1;
color_idx = max(1, min(256, color_idx)); % 确保索引合法
vertex_colors = cmap(color_idx, :); % 顶点对应的颜色
% 绘制多面体表面(通过顶点颜色插值显示体表纹样)
cla; % 清空当前轴
patch('Vertices', vertices, 'Faces', faces, ...
'FaceColor', 'interp', 'FaceVertexCData', vertex_colors, ...
'EdgeColor', [0.3 0.3 0.3], 'LineWidth', 0.5, 'FaceAlpha', 0.9);
axis equal; % 等比例显示
axis off; % 隐藏坐标轴
view(3); % 三维视角
camlight('headlight'); % 头部灯光(增强立体感)
lighting gouraud; % 平滑着色(提升表面过渡效果)
drawnow; % 刷新画面
% 控制帧间隔,确保总仿真时间为10秒
if frame < n_frames
pause(frame_delay);
end
end
% 单个多面体仿真结束后停留1秒再切换
pause(1);
close(fig);
end
disp('所有多面体波干涉仿真已完成!');
代码说明
-
数据与逻辑分离:
多面体的顶点 / 面 / 名称、颜色映射等静态数据通过独立脚本保存为 mat 文件,主代码仅通过load
加载,大幅减少冗余。 -
固定频率仿真:
移除了原代码中波源 1 的频率变化逻辑,f1
和f2
均设为固定值,符合 “频率不再变化” 的需求。 -
体表纹样绘制:
通过计算多面体顶点(体表点) 的波振幅,结合patch
函数的'FaceColor', 'interp'
参数,实现表面颜色插值,使干涉纹样均匀分布在多面体表面。 -
仿真时间控制:
通过dt
(时间步长)和frame_delay
(帧间隔)联动控制,确保每个多面体的仿真播放时间严格为 10 秒。
运行步骤:先运行save_plato_polyhedra.m
和save_color_config.m
生成数据文件,再运行polyhedron_wave_simulation.m
即可看到仿真效果。
效果比较魔幻,我也不知道是不是真的是这样子的,毕竟咱也没真正做过实验。

红色表示波峰,蓝色表示波谷?