刚开始看到ArtCraft这个名字的时候大多数人会以为它又是一个给游戏美术团队用的素材平台。但如果你在独立开发或者小团队里待过就会明白市面上缺的往往不是一个能画画的软件而是一个能让你把脑子里的立体造型快速搭出来、还不至于被复杂操作劝退的工具。ArtCraft 这个项目一开始只是我自己练手用的体素编辑器原型结果越做越顺手索性把它做成了一套可以正经出作品的完整流程。这篇文章就把我整个开发过程中的核心思路和技术选型记录下来包括数据结构的取舍、渲染管线的搭建、以及一个又一个的坑。1. 为什么偏偏叫 ArtCraft从一个体素迷的痛点说起体素这个概念并不新鲜但大部分现成的体素编辑器要么偏向涂鸦玩具要么就绑定了特定游戏的存档格式想拿来做点独立作品总感觉哪里别扭。我自己以前用别的工具画模型时最痛苦的就是两件事一是造型到一半想改某个细节结果整个区块的数据都得跟着重建二是导出到通用三维软件时网格质量一塌糊涂顶点乱飞。所以当决定自己写一个编辑器时目标就很明确它要能像搭积木一样自由又要能用工程化的方式管理数据最好还能一键导出干净的网格。ArtCraft 这个名字其实就是Art加Craft艺术和手工拼接的组合。我希望它给人的感觉不是冷冰冰的参数化建模工具而是一个类似做手工的工作台。你不用记一堆快捷键组合你只需要像捏粘土一样去增删方块、调整颜色、刷材质。整个项目的定位也由此确定面向独立创作者、原型设计师、像素艺术爱好者和游戏关卡原型师提供一套低门槛、高可控、可扩展的体素建模方案。这个定位也决定了我在技术选型上的方向。编辑器不能太过于依赖显卡的光线追踪特性因为用户的机器参差不齐数据结构不能一上来就追求极致压缩因为交互响应比节省几百兆内存更重要网络协议暂时不做因为单机稳定永远是第一位。很多项目野心太大结果死在一半我宁愿先把基础体验打磨到自己画得停不下来再去想协作和云端的事情。在实际动手之前我画了一张很简单的功能清单作为设计基准。第一版必须有基本选区、方块增删、颜色替换、大范围填充擦除、自动网格优化、撤销重做、导出 OBJ 和 GLTF。第二版才考虑镜像对称、平面切割、自定义调色板、外部模型导入、场景文件序列化。现在回看这份清单它救了我很多次因为只要新想法和这份清单冲突我就果断砍掉暂缓。2. 体素数据结构的选型Dense 数组之外的第三条路我最早写的原型用的是最朴素的三维数组也就是 Dense 存储。假设一个 16x16x16 的区域每个位置存一个颜色索引跑起来确实飞快做增删方块就是改一个数组下标完全没有任何心理负担。但一旦把尺寸放大到 128x128x64 这种稍微实用一点的范围内存和迭代开销立刻变得不友好。纯 Dense 存储若要支持多套材料属性体积会成倍上升更麻烦的是很多区域其实是空的空气所占的存储完全浪费了。后来我研究了常见体素渲染引擎的做法发现现阶段比较流行的是三种路线线性八叉树、RLE 行程编码、以及区块化稀疏存储。我自己在 ArtCraft 里采用的是区块化方案也就是把整个建模空间划分成很多个小 cube chunk。每个 chunk 内部仍然用一个相对紧凑的字节序列存储体素类型和颜色索引但只对非空气区域做实体化描述。这样做的优点是当你在编辑器的某一小块区域加方块时只需要在对应 chunk 上做局部更新完全不用重建全局结构撤销操作也可以以 chunk 为粒度做快照。这里有一个很关键的取舍要讲清楚。既然目标是像手工一样创作那么体素模型的体积往往不会像地形那样动辄几百万个方块大多数作品集中在几万个到几十万个方块的量级。在这个体量下过度复杂的空间索引不但无法带来明显性能提升反而会显著增加代码维护难度。因此 ArtCraft 里默认的编辑空间是 256x256x160采用的 chunk 尺寸是 32x32x32。靠近编辑区的活跃 chunk 会被缓存成即时的 Dense 数组以方便算法处理而离编辑器视点较远的 chunk 则以紧凑序列存储等待状态同步。实际测试下来一个包含约 14 万方块的场景内存占用只有纯三维数组方案的六分之一左右而方块拾取、放置、填充这些高频操作的延迟基本都能控制在 1 毫秒以内。代码层面我定义了简单的体素数据类型。每个方块仅用一个字节记录其大类例如空气、实心、半透明、液体等再用另一个字节记录它在全局调色板中的颜色索引。这样每次渲染或物理检测时不需要去读取完整的材料属性结构性能确实友好。水体、玻璃、发光体这类特殊材料先不进入基础数据结构而是作为后续扩展属性挂在 super block 上面只有在特定操作时才被解压出来。3. 从方块到造型核心建模操作的设计思路数据结构只是地基真正让一个体素编辑器好用的是交互和操作算法。ArtCraft 的交互模型其实特别简单你拿一把虚拟方块刷子在三维网格上点一下它就往那个格子里放一个方块长按并拖动就能连续放置。点一下减号模式又可以连续删除。听起来是不是太简单了但把细节抠到位后你会发现这比那些拖拽一堆控制点的建模软件更容易出活。第一个要注意的细节是拾取精度。因为体素编辑是在网格对齐的格子上操作你看到的方块其实是一个个平行投影后的四边形表面。在拾取时我做了两步检测先用射线和当前 chunk 的包围盒相交得到进入网格空间的参数 t然后以这个 t 为起点沿着射线方向逐格子步进找到第一个非空气的体素格子。为了不让方块的角部在拾取时产生视觉偏差我把方格之间的边缘处理成略微内缩的边框模型渲染出的线框与实际数据严格对齐。这个小细节做起来花不了半天但观感提升非常明显。相比单纯的增删更关键的是选区操作。ArtCraft 里提供了三种选区工具正方体选区、球体选区、以及自由绘制选区。正方体选区本质上是两个三维坐标的最小/最大值的组合任何遍历只需要套两层循环。球体选区则要通过给定的半径计算出一个包含所有受影响格子的坐标集合对每个格子判断是否满足距离约束。自由绘制选区更复杂它的输入是屏幕上的一组手绘轨迹我会先把这些二维轨迹投影到当前对齐平面上然后做一次栅格化凡是与轨迹相交的格子都进入候选集。经过这层处理用户可以用鼠标像画遮罩一样精确划定修改范围非常直观。填充和擦除是体素编辑器里最需要小心的功能。如果直接用洪水填充遇到一个大而复杂的区域会慢到让人崩溃。我在这里采取了一个很实用的折中方案对普通填充操作使用基于队列的只扩散至非空气格子算法同时设置一个步数上限当达到几十万次操作后自动终止并提示用户缩小范围。对于大范围的平面填充则会先切出该平面对应的二维区域在二维层面完成连通性判断后一次性回写体素数据速度和稳定性都有保障。填充算法里还有一个比较隐蔽的问题颜色混合。当你用新颜色覆盖旧颜色时如果完全按格子逐位替换边缘处容易留下接缝感的单层体素渲染后像是裂缝。这是因为相邻体素的法线方向差异突然变大光照接续不上。后来我给填充功能加了一个颜色边缘渐变选项默认关闭开启后会对填充边界做一圈只影响颜色索引的过渡扩散以 3x3x3 邻域做加权平均。虽然它不是物理层面的平滑但在视觉上能有效软化体素特有的锯齿边很多用户都反馈这个功能莫名解压。撤销和重做我采用的是多级快照方案。由于体素数据大部分在 chunk 内存储每次操作结束后我只会把被改动过的 chunk 的紧凑序列保存到撤销栈里。如果一次操作跨越了大量 chunk那么快照会带上区块索引清单后续重做时直接按清单对比。这个经济策略最大的好处是内存开销小即使连续操作几百步也不会把内存打成气球。我实际测试过连续删除一片 80x80x20 的实体区域每次撤销耗时基本在 15 毫秒以内完全不影响创作节奏。对于追求极致实时性的用户还可以开启自动压缩快照系统会每隔几次操作做一次增量合并代价是撤销历史的粒度变粗。4. 渲染那几步把体素变成顺畅画面的关键管线体素编辑器的画面流畅度直接决定创作体验。如果一个画笔下去要等上两三帧才看到画面更新你会下意识地认为这个工具很钝。ArtCraft 的渲染管线并不走复杂的实时光追而是老老实实用传统光栅化配合高效的网格生成。核心原则是永远只渲染与空气相邻的六面体表面也就是常见的 greedy mesh 优化思路。网上的 greedy mesh 教程很多但多数只是针对单一颜色实体碰到多颜色、多材料、半透明时就会露怯。我在 ArtCraft 里用的是一种简化但稳定的实现先沿着 Z 轴逐层扫描每个单元尝试在 X-Y 平面内合并同一材料的单元面形成更大的矩形然后再对 Y 轴做一次类似的合并。这样生成的面数相比一个格子六个面的暴力方案大概可以减少 70% 到 80%。对一个 100x100x50 的雕像模型暴力方案可能会有几百万个三角形而经过合并处理后可以降到 20 万左右普通独立显卡已经可以轻松跑满 60 帧。材质渲染上我没有引入过于复杂的 PBR 参数给每个体素材料定义了三项基本属性基础颜色、粗糙度、以及自发光强度。在着色阶段我用一个简单的 Blinn-Phong 高光配合方向光的阴影贴图来做光照效果好过大部分同级别的体素编辑器且场景构建非常简单。半透明材料如水、玻璃会被单独分到一个渲染批次按距离排序后优先绘制深度值较大的部分然后启用混合模式这样能避免透明物体相互遮挡时出现的明显错误。真正让画面有立体感的点是法线计算。传统做法是为每个单元面指定固定的朝向法线这在几何体表面平坦时看起来还行但遇到台阶状和起伏边缘时光照会显得特别生硬。我在 ArtCraft 里对每个顶点做了一次基于邻域体素分布的法线扰动。具体说来就是检查该方格周围哪些方向存在邻接方块如果某个方向没有邻接就对法线做一个微小的偏移让倾斜面看起来更平滑。虽然物理上不太严谨但从观感上接近于柔和阴影的效果体素建筑一下子就从像素块升级成棱角分明的立体模型。渲染管线的另一个大头是视锥裁剪和场景更新。ArtCraft 使用很简单的场景图来组织 chunk但每个 chunk 拥有一份独立的顶点缓冲。当编辑操作发生我只重新生成被修改的 chunk 对应的网格然后上传到 GPU。这样即使画面里同时存在几百个 chunk也只有少数几个需要更新性能压力就转移到了网格生成算法这一侧。为了让网格生成足够快我把所有循环尽量做成可并行的形式先用外循环收集所有被修改的候选面再用内部分块任务队列交给工作线程处理最后在主线程完成 buffer 提交。实测一个约 30x30x30 区域的快速涂刷操作从数据变更到画面反映出来的链路大概 8 毫秒基本达到了所见即所得。5. 导出与生态让作品不局限于编辑器如果只是在内置编辑器里画得开心那这个工具的价值就打折扣了。真正能融入创作者工作流的必须是开放的导出能力和稳定的文件格式。ArtCraft 在项目初期就定下目标既能输出常见的 OBJ 格式又能输出更现代的 GLTF 2.0 格式并且保证导出后的模型在第三方软件里的表现与编辑器预览基本一致。OBJ 导出相对简单但有一个容易踩坑的地方顶点坐标轴的朝向。不同的三位软件使用不同的坐标轴约定如果直接按照原始数据写导入进去模型可能是倒的。我的做法是在导出设置里提供 Y-UP 和 Z-UP 两个选项默认使用 Y-UP这是大多数游戏引擎和建模软件的标准。颜色信息方面OBJ 使用基础的 MTL 文件来定义每个材质的颜色和粗糙度确保在不同的查看器里都有一致的观感。GLTF 导出则要做更多工程化的处理。除了网格数据还得把调色板映射为独立的材质实例把自发光、金属度、透明度的参数一一对应到 GLTF 的扩展字段里。因为 GLTF 是二进制格式如果直接先写文本再转换会浪费内存和时间我在实现时直接将网格数据打包为二进制存取块再拼接 JSON 描述头。对于一张 20 万面的场景导出耗时基本控制在 2 到 3 秒左右。导出之前我还会执行一次顶点去重和网格清理把隐藏的重叠面移除进一步减小文件体积。不少用户希望把 ArtCraft 里的场景直接放进游戏引擎或者在 3D 打印软件里切片处理。为了覆盖这些场景我额外加入了固定大小的尺寸校准功能。比如约定一个体素对应的真实尺寸用户可以把设置切换为1 体素 1 毫米导出 OBJ 时就会按这个比例缩放模型。3D 打印条件下一个立方体的底座可能会因为悬空结构需要自动增加支撑所以我又加了一个简单的体素镂空检查自动识别所有内部封闭空腔并允许导出时将这些空腔填充为实体。这样打印出来的模型不光外观完整也不需要后期手动修补。我还做了一套轻量的场景文件格式扩展名为 ARC里面包含了调色板、地图数据、相机视角和操作历史。这个格式没有采用常见的数据格式重造轮子而是直接把 JSON 头加上二进制块拼在一起解析起来特别直接。每当你 CtrlS 保存场景系统都会把当前活跃 chunk 的紧凑序列序列化再附带一个全局哈希值。有了哈希值之后下次打开场景能迅速判断哪些 chunk 不需要重复加载因此大场景的打开速度也能保持稳定。6. 填坑记录我踩过的几个典型问题开发过程中踩过的坑林林总总这里挑几个最典型的记录一下每个拉出来都能节约不少人半天时间。第一个坑是贪婪网格合并时的颜色边界错误。早期我直接按照资料上的算法把同色方块合并成大面结果相邻不同颜色但几何连续的表面出现了大量三角形跳跃。这是因为贪婪网格的边长不一致在跨颜色的边界处法线插值会产生突变。修复方法倒不复杂在每个候选矩形的合并过程中多判断一次相邻格子的调色板索引一旦不一致就强制截断同时把顶点切线信息一并生成避免双色交界处的阴影异常。第二个坑是半透明体素的排序渲染问题。当透明块和非透明块交错存在于同一视线路径时先画谁、后画谁直接决定了最终颜色。我第一版直接把透明块和非透明块混在同一批次渲染结果玻璃内部的碎片显示成一片混乱。后来我调整策略将场景中的所有非透明网格先渲染然后关闭深度写入但保留深度测试再绘制透明网格并按照它们到相机的距离排序。这样分批次之后体素玻璃终于有了通透效果。第三个坑比较冷门是撤销栈和 chunk 合并的冲突。当两次小操作恰好落在同一个 chunk 的不同区域快照会保存该 chunk 的完整初始状态这本来没问题。但如果一次撤销只应回滚其中一个区域另一个区域的操作要保留直接恢复整个快照就会把后者的操作抹掉。解决办法是给每个 chunk 增加一个操作区间列表在每个快照里记录具体操作的网格坐标范围。撤销时只恢复这个坐标范围内的数据其他区域的数据保留不动。第四个坑和编辑器响应速度有关可能更适合有渲染经验的人参考。有一次我升级了顶点缓冲的更新策略做成了标记所有待更新 chunk下一帧统一提交结果一遇到大量小操作GPU 缓冲区的更新额高达数百兆帧率瞬间掉到 20 多帧。后来我判断问题不在于提交频率而在于每帧都执行了 buffer orphan频繁分配新显存。最后的优化是每帧只处理一个 chunk 的更新请求其余都排入队列并复用空闲 buffer这才把帧率稳定回来。这个经验告诉我体素编辑器的瓶颈往往不是算法复杂度而是你的更新节奏设计过于激进。7. 分享一个小技巧用画笔模拟圆滑边缘身体素模型总给人硬边的印象但很多作品其实可以通过颜色技巧来模拟圆滑边缘。我在 ArtCraft 里试验过一个很有意思的思路在目标形状的边缘处不直接换成相邻的颜色而是插入一到两层过渡色让表面产生像木雕打磨过的渐变感。因为体素本质上是离散的方块过度色层不会增加很多数据量但视觉上能模拟出圆角的反射变化。这个方法特别适合做像素风格的卡通角色很多人第一眼都以为我用了曲面建模。如果你也打算做一个体素编辑器我的建议是别一上来就追求无限世界或者多人协同先把核心的单人创作循环调到最舒服把导出格式做干净这个领域其实没有那么多竞争愿意认真打磨基础体验的人少之又少。ArtCraft 目前已经可以支撑我每天快速记录一些想法性草图偶尔也出片用来演示小关卡原型效果比预想中好不少。后面我大概率会继续补充导入 MOCUP 格式的支持让积木爱好者和体素创作者有一座现成的桥。说到底好的工具不是功能越多越好而是让你想表达的东西能马上表达出来这个道理放到任何创作软件上都成立。