- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
本技术指南围绕 Coding Train 第 63 号挑战的第二部分展开:在基于 toxiclibs Verlet 物理引擎搭建的 3D 布料网格之上,把 Unikitty 图像作为纹理贴到一面迎风飘动的旗帜上。阅读本文后,你将掌握 Processing 中beginShape()/endShape()/texture()的完整贴图渲染流程、TRIANGLE_STRIP 三角形带与 UV 坐标映射原理,以及用柏林噪声驱动粒子受力实现布料飘动效果的完整可运行方案。
挑战背景:两集联动的纹理旗帜
本挑战(63.2)是对 63.1 的延续,两集共享同一仓库 CodingChallenges/CC_063_unikitty_flag:
- 063.1-unikitty-flag.md:讲解如何用
beginShape()、endShape()、texture()渲染带图像纹理的几何体,讨论TRIANGLE_STRIP、QUAD_STRIP等不同形状模式的选择; - 本文对应的 063.2:把 Unikitty 图片作为纹理贴到由 toxiclibs Verlet 物理创建的飘动旗帜上。
因此本文既包含 63.1 的贴图渲染核心概念,也完整展开 63.2 中"物理网格 + 纹理"的整合实现。仓库中的完整源码位于 CC_063_unikitty_flag.pde,配套的 Particle.pde 与 Spring.pde 分别定义了粒子与弹簧类。
第一步:用 toxiclibs 搭建 3D 布料网格
旗帜的本质是一张由粒子(VerletParticle3D)与弹簧(VerletSpring3D)交织而成的 40×40 网格。主程序开头引入 toxiclibs 的三维物理与几何包:
import toxi.physics3d.*; import toxi.physics3d.behaviors.*; import toxi.physics3d.constraints.*; import toxi.geom.*;粒子网格初始化
int res = 1; int cols = 40 / res; // 列数 int rows = 40 / res; // 行数 Particle[][] particles = new Particle[cols][rows]; float w = 10 * res; // 相邻粒子的间距网格以屏幕中心为原点对称铺开,每个粒子沿 x、y 方向间隔w:
float x = -cols*w/2 - 100; // x 起点左移 100,为旗杆留出空间 for (int i = 0; i < cols; i++) { float y = -rows*w/2; for (int j = 0; j < rows; j++) { Particle p = new Particle(x, y, 0); particles[i][j] = p; physics.addParticle(p); y = y + w; } x = x + w; }Particle.pde 中的粒子类极简,直接继承 toxiclibs 的VerletParticle3D,仅在调试时用display()绘制 2 像素小圆点:
class Particle extends VerletParticle3D { Particle(float x, float y, float z) { super(x, y, z); } void display() { pushMatrix(); translate(x, y, z); noStroke(); fill(255); ellipse(0, 0, 2, 2); popMatrix(); } }弹簧连接:右邻与下邻
遍历每个粒子,若存在右邻(i+1)或下邻(j+1),就创建弹簧并注册到物理世界:
for (int i = 0; i < cols; i++) { for (int j = 0; j < rows; j++) { Particle a = particles[i][j]; if (i != cols-1) { Particle b1 = particles[i+1][j]; Spring s1 = new Spring(a, b1); springs.add(s1); physics.addSpring(s1); } if (j != rows-1) { Particle b2 = particles[i][j+1]; Spring s2 = new Spring(a, b2); springs.add(s2); physics.addSpring(s2); } } }Spring.pde 中,弹簧的静止长度直接取全局间距w,强度为 1(该值越大布料越"硬"):
class Spring extends VerletSpring3D { Spring(Particle a, Particle b) { super(a, b, w, 1); } void display() { stroke(255); strokeWeight(1); line(a.x, a.y, a.z, b.x, b.y, b.z); } }横向对比:第 20 号挑战 CC_020_ClothSimulation 中的 2D 版本 P5/spring.js 使用同样的
super(a, b, w, 1)构造方式,而 3D 版本 CC_020_Cloth3D.pde 通过particles[0][0].lock()与particles[cols-1][0].lock()锁定左上、右上两个角。本挑战则锁定整条左边缘(见下文)。
固定左边缘与重力
旗帜需要挂在旗杆上,因此锁定第一列全部粒子:
for(int i = 0; i < particles[0].length; i += 1){ particles[0][i].lock(); }物理世界初始化时注册重力行为(Vec3D(0, 0.5, 0)表示 y 轴方向的轻微重力,数值比第 20 号挑战的Vec3D(0, 1, 0)更小,让旗帜更松弛):
physics = new VerletPhysics3D(); Vec3D gravity = new Vec3D(0, 0.5, 0); GravityBehavior3D gb = new GravityBehavior3D(gravity); physics.addBehavior(gb);第二步:柏林噪声驱动的三维风力
布料的"飘动"并非真实流体模拟,而是每一帧对每个粒子施加一个由noise()计算的风力向量。核心技巧是给噪声输入叠加不同的偏移量(+5000、+3000),让 x、y、z 三个方向的风力互不相关:
float xoff = 0; for (int i = 0; i < cols; i++) { float yoff = 0; for (int j = 0; j < rows; j++) { float windx = map(noise(xoff, yoff, zoff), 0, 1, 0, 3); // 主导风向,向右 float windy = map(noise(xoff+5000, yoff+5000, zoff), 0, 1, -0.5, 0); // 轻微向上 float windz = map(noise(xoff+3000, yoff+3000, zoff), 0, 1, -1, 1); // 前后摆动 Vec3D wind = new Vec3D(windx, windy, windz); particles[i][j].addForce(wind); yoff += 0.1; } xoff += 0.1; } zoff += 0.1; // 每帧推进第三个噪声维度,形成随时间变化的流动参数要点:
map(noise(...), 0, 1, 0, 3)将噪声输出从 [0,1] 映射到风力区间;x 方向取 0~3 是主风力,y 方向取 -0.5~0 让旗帜略微上飘,z 方向取 -1~1 提供纵深摆动;xoff/yoff每次递增 0.1 控制风力在网格上的空间变化频率,值越小风越"平滑";zoff每帧递增 0.1,是风随时间演化(周期性飘动)的关键。
随后调用physics.update()让 Verlet 积分器推进所有粒子位置。主循环还预留了rotateY(a)的旋转演示代码(默认注释),可取消注释观察旗帜的 3D 形态。
第三步:TRIANGLE_STRIP 与 UV 贴图渲染
这是本挑战(也是 63.1)的核心:如何把 Unikitty 图像贴到随粒子位置变形的网格上。
加载纹理
PImage unikitty; // ...setup 末尾... unikitty = loadImage("unikitty.jpg");纹理图像存放在仓库的 CodingChallenges/CC_063_unikitty_flag/Processing/CC_063_unikitty_flag/data/unikitty.jpg(2448×2448 的方形图片),Processing 会从 sketch 的data目录加载它。
渲染循环:三角形带 + 纹理坐标
noFill(); noStroke(); textureMode(NORMAL); // 纹理坐标使用归一化 [0,1] for (int j = 0; j < rows-1; j++) { beginShape(TRIANGLE_STRIP); texture(unikitty); for (int i = 0; i < cols; i++) { // 上一行顶点 float x1 = particles[i][j].x; float y1 = particles[i][j].y; float z1 = particles[i][j].z; float u = map(i, 0, cols-1, 0, 1); float v1 = map(j, 0, rows-1, 0, 1); vertex(x1, y1, z1, u, v1); // 下一行顶点 float x2 = particles[i][j+1].x; float y2 = particles[i][j+1].y; float z2 = particles[i][j+1].z; float v2 = map(j+1, 0, rows-1, 0, 1); vertex(x2, y2, z2, u, v2); } endShape(); }实现要点逐条拆解:
textureMode(NORMAL):声明后续vertex(x, y, z, u, v)中的纹理坐标是 0~1 的归一化值,而非像素值;TRIANGLE_STRIP模式:每行两两顶点(第 j 行与第 j+1 行)交替写入,构成一条连续的三角形带。这种方式相比逐个绘制独立三角形,顶点数量几乎减半,是 GPU 渲染布料/地形的标准做法。63.1 中讨论的QUAD_STRIP是另一种选择,但三角形带在纹理插值上更通用;- UV 坐标映射:
u = map(i, 0, cols-1, 0, 1)、v = map(j, 0, rows-1, 0, 1)把网格行列索引线性映射到纹理坐标。由于顶点坐标取自粒子的实时物理位置,而 UV 坐标由固定行列索引算出,旗帜飘动时图像就会随之扭曲——这正是"飘动的 Unikitty 旗帜"的视觉效果来源; noFill()与noStroke():渲染纯纹理表面,避免出现线框干扰。
最后绘制旗杆,把旗帜左边缘与一条竖线对齐:
stroke(255); strokeWeight(4); line(-cols*w/2 - 100, -rows*w/2, -cols*w/2 - 100, height);调试时可取消注释//for (Spring s : springs) s.display();与粒子display(),直接看到物理网格结构。
完整可运行代码
将 CC_063_unikitty_flag.pde、Particle.pde、Spring.pde 三个文件放入同一 sketch 目录,并把unikitty.jpg放入data子目录,即可在 Processing(需安装 toxiclibs 库)中运行。渲染窗口为size(800, 600, P3D),启用 3D 渲染器。
p5.js 移植参考
仓库中同一系列的 2D 布料(CC_020_ClothSimulation/P5)提供了 p5.js 移植的完整参照:页面按顺序引入 p5.js、toxiclibs.js 与 toxichelper.js(见 index.html),JS 侧同样遵循"粒子入物理世界 → 弹簧连接 → 锁定锚点 → 每帧 update"的结构。若要把本挑战移植到 p5.js,仅需将三维类替换为VerletPhysics3D对应接口、loadImage改为异步加载,并保持beginShape(TRIANGLE_STRIP)+texture()+ 归一化 UV 的渲染管线不变。
参数调节指南
| 参数 | 位置 | 效果与建议 |
|---|---|---|
cols/rows(40) | 主程序顶部 | 网格密度。增大可让旗帜更细腻,但粒子与弹簧数量平方级增长,注意性能 |
w(10) | 主程序顶部 | 粒子间距,同时决定弹簧静止长度与旗帜整体尺寸 |
弹簧强度(super(a, b, w, 1)的第三参) | Spring.pde | 越大布料越硬挺,越小越柔软、摆动幅度越大 |
重力Vec3D(0, 0.5, 0) | setup | 增大使旗帜下垂更明显,减小则更轻盈 |
风力区间0~3/-0.5~0/-1~1 | draw | 控制三个方向的风力强度,主导飘动幅度与方向 |
xoff/yoff步长 0.1 | draw | 风力在网格上的空间变化频率 |
zoff步长 0.1 | draw | 风随时间演化的速度,越大飘动越急促 |
常见问题
- 纹理未显示:确认调用了
textureMode(NORMAL)、beginShape()后立即texture(unikitty),且vertex()传入 5 个参数(x, y, z, u, v);纹理坐标必须落在 [0,1] 区间(归一化模式下)。 - 旗帜完全不飘:检查
physics.update()是否在每帧draw()中调用,以及是否在draw()中为每个粒子执行了addForce(wind)。 - 旗帜脱离旗杆:确认左边缘粒子已通过
particles[0][i].lock()锁定;若只锁两端,边缘中部会下垂。 - 画面只有线框:确认渲染循环中已设置
noFill(); noStroke();,或检查是否误把调试用的弹簧/粒子display()保留在纹理绘制之后。
延伸阅读
- 本挑战第一部分:063.1-unikitty-flag.md,深入讲解
beginShape()/endShape()/texture()与TRIANGLE_STRIP、QUAD_STRIP的取舍; - 布料物理基础:020-cloth3d.md 与 CC_020_ClothSimulation,展示无纹理的 2D/3D 布料网格搭建;
- 自定义形状:9.22-custom-shapes.md 与 18.8-custom-shapes-in-webgl-with-p5js.md,介绍 p5.js 中
beginShape()、vertex()的通用用法。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
用 Processing 为 3D 布料模拟贴上旗帜纹理:Unikitty Flag 挑战(CC 063.1)源码全解析
用 Processing 为 3D 布料模拟贴上旗帜纹理:Unikitty Flag 挑战(CC 063.1)源码全解析 导读 本文以 Coding Train
教程文档示例工程旗帜数据提供者:Essentials中的旗帜元数据处理方案
旗帜数据提供者:Essentials中的旗帜元数据处理方案 在Minecraft服务器开发中,旗帜(Banner)作为一种具有丰富自定义能力的物品,其元数据处理
游戏开发Hindsight API网关:统一管理和保护记忆API的终极指南
Hindsight API网关:统一管理和保护记忆API的终极指南 Hindsight作为一款专注于AI智能体记忆管理的开源项目,其API网关是实现记忆统一管理
人工智能AI AgentAgent 记忆MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考