Godot地形纹理混合画笔工具开发:从原理到实战实现
1. 项目概述从“硬边”到“自然”的创作革命如果你用过Godot或者Unity这类引擎做过3D场景尤其是地形编辑大概率都经历过一个共同的痛点不同纹理之间的过渡太生硬了。草地和泥土、沙地和岩石它们之间的交界处就像用剪刀裁出来的一样一条清晰、笔直的线这在自然界中几乎是不存在的。这种“硬边”会瞬间打破场景的沉浸感让玩家一眼就看出这是“做出来的”而非“自然生长的”。过去要解决这个问题要么依赖引擎内置的、功能有限且往往不够直观的笔刷要么就得在外部3D软件如Blender中预先绘制好复杂的混合贴图流程繁琐迭代成本极高。这个项目标题——“告别重复劳动Godot地形纹理混合术用画笔工具实现自然过渡”——精准地戳中了这个痛点。它的核心价值在于将专业级的地形纹理混合能力以一种高度直观、艺术家友好的“画笔工具”形式集成到Godot引擎的实时编辑工作流中。这不仅仅是技术实现更是一种工作流的解放。想象一下你不再需要反复导出模型、在PS里涂抹、再导回引擎测试效果而是像在画布上作画一样直接在Godot的3D视口中用画笔实时地、可视化地控制草地如何逐渐渗入泥土岩石的苔藓如何在边缘生长。这大大降低了美术门槛提升了创作效率和最终效果的质量。从技术角度看它涉及Godot的着色器编程、纹理采样、顶点颜色或自定义数据的利用以及编辑器插件的开发。而从应用场景来说它几乎是一切需要自然地貌的3D项目的刚需无论是开放世界游戏、RPG场景、模拟经营地图还是影视级的实时渲染预览。结合你提供的热词如“Godot双瓦片系统”这个技术甚至可以与之结合在基于瓦片Tile的地形系统上实现更精细的局部混合控制潜力巨大。接下来我将彻底拆解这个功能的实现思路、核心技术细节、完整的实操步骤并分享我在集成此类功能时踩过的坑和总结的经验。无论你是刚接触Godot着色器的新手还是寻求工作流优化的资深开发者这篇内容都能提供一条从零到一的清晰路径。2. 核心原理与架构设计理解“混合”是如何发生的在动手写代码之前我们必须搞清楚目标我们究竟要混合什么以及在哪里混合。这是所有后续工作的基石理解错了后面全是白费功夫。2.1 纹理混合的本质权重与插值地形纹理混合不是简单地把两张图片叠在一起。它的核心思想是“权重控制下的线性插值”。假设我们有两张纹理Texture_Ground泥土和Texture_Grass草地。在任何一个给定的像素点或顶点它最终显示的颜色FinalColor应该是这两张纹理在该点颜色的混合FinalColor Grass_Color * weight Ground_Color * (1 - weight)这里的weight权重是一个介于 0.0 到 1.0 之间的值。当weight 0.0最终颜色完全等于Ground_Color纯泥土。当weight 1.0最终颜色完全等于Grass_Color纯草地。当weight 0.5最终颜色是泥土和草地颜色各取一半的混合。我们的“画笔工具”本质上就是一个在模型表面通常是顶点或纹理坐标层面绘制和修改这个weight值的工具。画笔划过的地方weight值被改变从而改变了该区域两张纹理的混合比例实现了视觉上的过渡。2.2 权重数据的存储顶点的“隐藏画布”那么这个动态的weight值存储在哪里呢我们不能每帧都去计算它需要被持久化。常见且高效的方案是利用模型的顶点颜色Vertex Color或顶点自定义数据Custom Vertex Data。为什么是顶点数据性能与兼容性顶点数据是模型固有的属性GPU在渲染管线早期就能获取非常适合在着色器中进行计算。几乎所有渲染管线都支持顶点颜色。空间连续性顶点之间的值会自动进行平滑插值除非你特意设置不插值这正好符合我们“自然过渡”的需求。你在一个顶点上画了权重1纯草在相邻顶点画了权重0纯泥它们之间的区域会自动平滑过渡。存储效率一个顶点颜色有RGBA四个通道每个通道0-1。我们可以用其中一个通道比如R通道来存储一种混合权重。这意味着理论上一套顶点颜色可以控制最多4种纹理之间的混合虽然实际会复杂一些。方案选型R通道存储权重这是最直接、最常用的方案。我们将地形的所有顶点颜色初始化为(0, 0, 0, 1)即黑色Alpha为1。画笔工具的工作就是修改指定区域内顶点的R通道值。在着色器中我们读取vertex_color.r直接作为上面公式中的weight。注意使用顶点颜色意味着混合的精度和细节受限于模型的顶点密度。在非常平坦、顶点稀疏的区域你可能无法绘制出精细的混合边缘。这时需要考虑细分模型或使用纹理贴图如一张单独的控制图来存储权重但后者会引入额外的纹理采样开销和资产管理复杂度。对于大多数中大型地形合理布线的网格配合顶点颜色是完全够用的也是性能最优的选择。2.3 系统架构总览整个功能可以拆分为三个核心模块它们协同工作编辑器插件模块画笔工具UI与逻辑在Godot编辑器中添加一个新的侧边栏或工具栏。提供画笔参数设置半径、强度、混合模式添加、平滑、覆盖等。监听鼠标在3D视口中对特定MeshInstance节点的点击、拖拽事件。计算画笔影响范围内的顶点并修改它们的顶点颜色数据。数据处理模块顶点权重计算与更新这是画笔工具的核心算法部分。当画笔在模型表面移动时需要 a.射线检测从屏幕鼠标位置发射射线与地形网格碰撞获取碰撞点位置和碰撞到的MeshInstance。 b.顶点搜索以碰撞点为中心根据画笔半径找到该MeshInstance网格数据中所有位于范围内的顶点。 c.权重计算根据顶点到碰撞点的距离计算一个衰减后的强度值然后按照当前画笔的强度和模式更新这些顶点的颜色R值。 d.数据提交将修改后的顶点数组数据重新提交给MeshInstance的Mesh从而更新渲染。着色器模块实时渲染混合效果一个自定义的ShaderMaterial附加到地形模型上。在片段着色器Fragment Shader中 a. 采样Texture_Ground和Texture_Grass。 b. 读取该片段插值后的vertex_color.r作为混合权重weight。 c. 根据公式进行线性插值输出最终颜色。 d. 高级可以引入第三张、第四张纹理利用顶点颜色的G、B通道进行更复杂的混合。理解了这套架构我们就知道每一步代码要写在哪里以及它们之间如何通信。下面我们进入具体的实现环节。3. 实战构建Godot地形纹理混合画笔工具我们将按照“从内到外”的顺序来构建先实现核心的着色器确保混合效果能正确显示然后再构建编辑器工具来操控数据。3.1 第一步创建基础混合着色器在Godot中创建一个新的ShaderMaterial并为其编写着色器。这是效果的视觉基础。// 创建一个新的 ShaderMaterial然后在 Shader 属性中粘贴以下代码。 shader_type spatial; // 用于3D物体的着色器类型 // 定义 uniforms这些是可以在编辑器和代码中调整的参数 uniform sampler2D texture_ground : source_color; uniform sampler2D texture_grass : source_color; uniform float uv_scale 5.0; // 控制纹理的重复度让纹理更密集 void fragment() { // 计算缩放后的UV坐标 vec2 scaled_uv UV * uv_scale; // 采样两种纹理的颜色 vec4 ground_color texture(texture_ground, scaled_uv); vec4 grass_color texture(texture_grass, scaled_uv); // 关键步骤从顶点颜色的R通道读取混合权重。 // 这里使用 COLOR.r在Godot的Spatial着色器中COLOR 内置变量代表插值后的顶点颜色。 float blend_factor COLOR.r; // 线性插值混合。mix函数是GLSL内置的等同于 ground_color * (1.0 - blend_factor) grass_color * blend_factor vec4 final_color mix(ground_color, grass_color, blend_factor); // 输出最终颜色和粗糙度这里简单处理你也可以分别混合粗糙度贴图 ALBEDO final_color.rgb; ROUGHNESS 0.8; // 给一个默认的粗糙度值 }操作解析与注意事项uv_scale这是一个非常重要的参数。如果直接使用模型UV通常一个地形网格只占UV空间的0-1范围导致纹理被拉伸得非常模糊。通过乘以一个大于1的系数如5.0或10.0我们让纹理在模型表面重复多次从而获得更清晰的细节。这个值需要根据地形的实际尺寸和纹理分辨率来调整。COLOR变量在Godot的spatial着色器中COLOR直接对应了模型的顶点颜色。这是实现混合的关键数据源。mix函数GLSL标准函数用于线性插值代码更简洁。粗糙度与法线这是一个基础示例。在实际项目中你很可能还需要混合粗糙度贴图、法线贴图甚至高度贴图来获得真实的表面质感。混合原理相同只需为每种贴图都进行类似的mix操作即可。这会增加着色器的复杂度和采样次数但效果提升显著。将写好的ShaderMaterial赋给你的地形模型并为texture_ground和texture_grass分配两张不同的纹理。此时如果你手动去修改网格的顶点颜色例如在Blender中回到Godot就能立刻看到混合效果。这验证了着色器部分是工作的。接下来我们要在Godot内部动态地修改顶点颜色。3.2 第二步开发编辑器画笔插件这是项目的核心我们需要编写一个EditorPlugin。在Godot项目的addons/目录下创建一个新文件夹例如terrain_painter/然后创建以下文件addons/terrain_painter/plugin.gdtool extends EditorPlugin var paint_tool: Control var brush_radius: SpinBox var brush_strength: SpinBox func _enter_tree(): # 创建自定义的工具栏UI paint_tool preload(res://addons/terrain_painter/paint_tool_ui.tscn).instantiate() brush_radius paint_tool.get_node(%BrushRadius) brush_strength paint_tool.get_node(%BrushStrength) # 将UI添加到编辑器顶部工具栏 add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, paint_tool) paint_tool.hide() # 初始隐藏 # 连接UI按钮信号 paint_tool.get_node(%StartPaintButton).pressed.connect(_on_start_paint_pressed) paint_tool.get_node(%EndPaintButton).pressed.connect(_on_end_paint_pressed) func _exit_tree(): # 插件卸载时清理UI remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, paint_tool) paint_tool.free() func _on_start_paint_pressed(): # 显示画笔UI并开始监听3D编辑器的输入事件 paint_tool.show() # 这里需要获取3D视图并设置其鼠标模式通常需要更深入的编辑器集成 # 简化示例我们通过覆盖 _forward_3d_gui_input 来处理见下文 print(画笔工具激活基础框架) func _on_end_paint_pressed(): # 结束绘画隐藏UI恢复默认鼠标模式 paint_tool.hide() print(画笔工具结束)addons/terrain_painter/painter.gd这个脚本是真正的核心它应该被附加到一个单例AutoLoad或由插件直接管理负责具体的绘制逻辑。tool extends Node # 画笔参数 var is_painting : false var current_radius : 5.0 var current_strength : 0.5 var brush_mode : add # add, subtract, smooth # 当前正在绘制的网格和其表面数据缓存 var current_mesh_instance: MeshInstance3D null var surface_tool: SurfaceTool null var mesh_data: ArrayMesh null var original_vertex_array: PackedVector3Array var original_color_array: PackedColorArray func start_paint_on_mesh(mesh_instance: MeshInstance3D): if not mesh_instance or not mesh_instance.mesh: return current_mesh_instance mesh_instance mesh_data mesh_instance.mesh as ArrayMesh if mesh_data.get_surface_count() 0: return # 使用SurfaceTool来读取和修改网格数据 surface_tool SurfaceTool.new() surface_tool.create_from(mesh_data, 0) // 假设我们修改第0个表面 original_vertex_array surface_tool.commit_to_arrays()[Mesh.ARRAY_VERTEX] original_color_array surface_tool.commit_to_arrays()[Mesh.ARRAY_COLOR] is_painting true print(开始在网格上绘画: , mesh_instance.name) func paint_at_position(world_position: Vector3, normal: Vector3): if not is_painting or not current_mesh_instance: return var transform current_mesh_instance.global_transform var local_position transform.affine_inverse() * world_position # 遍历所有顶点找到在画笔半径内的 var verts original_vertex_array var colors original_color_array var modified false for i in range(verts.size()): var vertex_local verts[i] var distance vertex_local.distance_to(local_position) if distance current_radius: # 计算衰减强度线性衰减 var falloff 1.0 - (distance / current_radius) var paint_value current_strength * falloff # 根据画笔模式修改颜色R值 var old_color colors[i] var new_r old_color.r if brush_mode add: new_r clamp(old_color.r paint_value, 0.0, 1.0) elif brush_mode subtract: new_r clamp(old_color.r - paint_value, 0.0, 1.0) elif brush_mode smooth: # 平滑模式向周围顶点平均值靠拢这里简化处理为向0.5靠拢 new_r lerp(old_color.r, 0.5, paint_value * 0.1) if abs(new_r - old_color.r) 0.001: colors[i] Color(new_r, old_color.g, old_color.b, old_color.a) modified true if modified: # 将修改后的颜色数组写回SurfaceTool并更新网格 surface_tool.clear() surface_tool.begin(Mesh.PRIMITIVE_TRIANGLES) for i in range(verts.size()): surface_tool.set_color(colors[i]) surface_tool.add_vertex(verts[i]) # 这里需要重新添加索引简化起见假设是简单列表。实际项目需处理索引数组。 surface_tool.generate_normals() // 可选如果修改了顶点可能需要重新计算 var new_mesh surface_tool.commit() current_mesh_instance.mesh new_mesh # 更新缓存 original_color_array colors func stop_paint(): is_painting false current_mesh_instance null surface_tool null print(停止绘画)关键难点与实操心得性能上述示例中每帧或每次鼠标移动都遍历所有顶点是非常低效的。优化策略是使用空间加速结构如AABB轴对齐包围盒或Octree八叉树。你可以预先将网格顶点组织到基于位置的小格子中画笔操作时只计算与画笔球体相交的格子内的顶点。撤销/重做编辑器工具必须支持CtrlZ。Godot的EditorUndoRedoManager就是为此而生。在paint_at_position函数中不应该直接修改current_mesh_instance.mesh而应该创建一个可撤销的操作var undo_redo get_undo_redo() undo_redo.create_action(Paint Terrain) undo_redo.add_do_method(self, _apply_mesh_data, current_mesh_instance, new_mesh_data) undo_redo.add_undo_method(self, _apply_mesh_data, current_mesh_instance, old_mesh_data) undo_redo.commit_action()射线检测集成为了让painter.gd的paint_at_position被调用你需要在你创建的编辑器插件中重写_forward_3d_gui_input方法捕获3D视口中的鼠标事件进行射线检测获取碰撞的世界坐标和法线然后调用绘制函数。这是连接编辑器UI和3D场景的桥梁。3.3 第三步实现3D视口交互与射线检测在plugin.gd中补充输入处理逻辑func _forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) - bool: # 如果画笔工具未激活不处理输入 if not paint_tool or not paint_tool.visible: return false if event is InputEventMouseMotion and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): var viewport get_editor_interface().get_editor_viewport_3d(0) var mouse_pos viewport.get_mouse_position() # 从摄像机向鼠标位置发射射线 var ray_origin viewport_camera.project_ray_origin(mouse_pos) var ray_normal viewport_camera.project_ray_normal(mouse_pos) var space_state viewport.get_world_3d().direct_space_state var query PhysicsRayQueryParameters3D.new() query.from ray_origin query.to ray_origin ray_normal * 1000.0 query.collide_with_areas true query.collide_with_bodies true # 可以设置碰撞层确保只与地形碰撞 # query.collision_mask 1 (your_terrain_layer - 1) var result space_state.intersect_ray(query) if result: var collider result.collider if collider is MeshInstance3D: # 确保我们只绘制有我们特定ShaderMaterial的地形 # 可以检查collider.material是否是我们需要的材质 if not painter.current_mesh_instance or painter.current_mesh_instance ! collider: painter.start_paint_on_mesh(collider) painter.paint_at_position(result.position, result.normal) return true # 消耗此事件防止其他编辑器操作 elif event is InputEventMouseButton and event.button_index MOUSE_BUTTON_LEFT and not event.pressed: painter.stop_paint() return false这段代码将编辑器的3D视口鼠标输入转化为对场景中MeshInstance3D的射线检测并将碰撞点信息传递给我们的绘制器。4. 高级技巧与性能优化实战基础功能跑通后我们会立刻遇到性能和效果上的瓶颈。下面分享几个实战中提炼出的高级技巧。4.1 支持多纹理混合与通道分配只用R通道控制两种纹理的混合显然不够。一个自然的地形可能需要泥土、草地、岩石、沙地四种基础纹理。我们可以利用顶点颜色的RGBA四个通道。着色器升级uniform sampler2D texture_ground; uniform sampler2D texture_grass; uniform sampler2D texture_rock; uniform sampler2D texture_sand; uniform float uv_scale 5.0; void fragment() { vec2 scaled_uv UV * uv_scale; vec4 col_ground texture(texture_ground, scaled_uv); vec4 col_grass texture(texture_grass, scaled_uv); vec4 col_rock texture(texture_rock, scaled_uv); vec4 col_sand texture(texture_sand, scaled_uv); // 假设R草地权重G岩石权重B沙地权重 // 泥土作为基底权重为 1.0 - (RGB)但需确保总和不超过1 vec3 weights COLOR.rgb; float base_weight max(0.0, 1.0 - (weights.r weights.g weights.b)); // 混合计算线性混合 vec4 final_color col_ground * base_weight; final_color mix(final_color, col_grass, weights.r); final_color mix(final_color, col_rock, weights.g); final_color mix(final_color, col_sand, weights.b); ALBEDO final_color.rgb; }画笔工具升级你的UI需要提供选择当前绘制通道R/G/B的功能。painter.gd中的paint_value将作用于选中的通道。重要经验直接使用多个通道进行线性混合很容易导致权重和超过1产生不自然的过饱和区域。更高级的做法是使用“权重标准化”。一种常见技巧是在着色器中将weights COLOR.rgb进行归一化处理确保它们的和为1。但这会改变美术的直观控制。另一种方案是在绘制逻辑中施加约束例如当增加一个通道的权重时自动按比例降低其他通道的权重。这需要更复杂的画笔逻辑但对美术师更友好。4.2 引入笔刷贴图与衰减曲线目前我们的笔刷衰减是简单的线性衰减这画出来的边缘比较生硬。我们可以引入一张笔刷贴图如一张中心白边缘黑的圆形渐变图和可配置的衰减曲线。在painter.gd中改进var brush_texture: Texture2D preload(res://addons/terrain_painter/brushes/soft_round.tres) var brush_falloff_curve: Curve preload(res://addons/terrain_painter/curves/linear.tres) func paint_at_position(world_position: Vector3, normal: Vector3): # ... 前面的顶点遍历 ... for i in range(verts.size()): var vertex_local verts[i] var vec_to_vertex vertex_local - local_position var distance vec_to_vertex.length() if distance current_radius: // 计算在笔刷纹理UV空间中的位置 (从 -1,-1 到 1,1) var brush_uv_x vec_to_vertex.x / current_radius var brush_uv_y vec_to_vertex.z / current_radius // 假设在XZ平面绘制 var brush_uv Vector2(brush_uv_x, brush_uv_y) * 0.5 Vector2(0.5, 0.5) // 从笔刷贴图采样强度 var texture_strength brush_texture.get_image().get_pixelv(brush_uv * brush_texture.get_size()).r // 应用衰减曲线 var distance_ratio distance / current_radius var curve_strength brush_falloff_curve.sample(distance_ratio) // 综合计算最终绘制强度 var falloff texture_strength * curve_strength var paint_value current_strength * falloff # ... 后续修改颜色逻辑 ...这样美术师就可以通过更换不同的笔刷贴图硬边、软边、噪波边等和调整衰减曲线创造出极其丰富的笔触效果从平滑过渡到斑驳的肌理都能实现。4.3 性能优化空间划分与批量更新遍历所有顶点是O(n)的复杂度当地形顶点数上万时实时绘制会卡顿。必须引入空间划分。简化版网格空间划分实现思路预处理在start_paint_on_mesh时遍历一次所有顶点根据其世界坐标或局部坐标将其索引放入一个字典Dictionary中字典的键是离散化的网格坐标如int(vertex.x / cell_size), int(vertex.z / cell_size)值是该单元格内所有顶点的索引数组。绘制时计算画笔影响范围的AABB。快速计算出与这个AABB相交的所有网格单元格。只遍历这些单元格内的顶点索引。批量更新不要每次修改一个顶点就更新一次网格。将一次鼠标拖拽过程中的所有修改累积起来在鼠标释放时或每N毫秒进行一次网格提交。Godot的SurfaceTool或直接操作ArrayMesh的surface_get_arrays和surface_set_arrays可以用于批量更新。这个优化能将计算量降低1-2个数量级是实现流畅手绘体验的关键。5. 常见问题、排查技巧与避坑指南在实际集成和使用的过程中你一定会遇到各种各样的问题。这里记录了一些典型问题和解决方法。5.1 问题绘制没有反应颜色不改变排查步骤检查着色器首先确认你的ShaderMaterial是否正确赋给了地形模型并且COLOR.r确实被用于混合。可以在片段着色器开头加一句ALBEDO vec3(COLOR.r, 0, 0);测试顶点颜色R通道是否被正确读取和可视化。检查顶点颜色数据Godot的某些网格导入设置或生成方式可能不包含顶点颜色。确保你的网格数据包含ARRAY_COLOR属性。可以在painter.gd的start_paint_on_mesh中打印original_color_array的长度确认它大于0。检查射线检测在_forward_3d_gui_input中打印result和result.collider确保射线确实击中了你的地形MeshInstance3D并且没有因为碰撞层设置而被过滤。检查绘制逻辑在paint_at_position的循环内部添加调试打印确认distance current_radius的条件是否被触发以及new_r的值是否被计算和修改。5.2 问题绘制时闪烁或性能极差原因与解决每帧更新网格如果每次paint_at_position可能在_input事件中每帧调用都直接current_mesh_instance.mesh new_mesh会造成巨大的性能开销。必须实现批量更新或节流更新。未做空间划分如4.3节所述这是性能问题的首要原因。务必实现基于网格或树的加速结构。着色器复杂度高如果你混合了多张法线贴图、高光贴图片段着色器计算量会很大。考虑使用纹理数组TextureArray或虚拟纹理Virtual Texture等更高级的技术来优化。对于移动平台要严格控制纹理采样次数。5.3 问题混合边缘有接缝或纹理拉伸原因与解决UV缩放问题确保着色器中的uv_scale设置合理。过小会导致纹理模糊过大会导致纹理重复感过强。可以为不同纹理设置不同的缩放值。纹理过滤与Mipmaps确保导入的纹理资源启用了Mipmaps并且过滤模式设置为“三线性”Trilinear或“各向异性”Anisotropic这能在视角倾斜时减少混合区域的闪烁和模糊。顶点颜色插值Godot默认对顶点颜色进行平滑插值。如果你想要更硬朗的边界需要在建模时增加顶点密度或者在着色器中使用flat关键字但需注意兼容性。另一种方法是使用纹理贴图存储权重这样可以获得像素级的精度但牺牲了性能和一些动态修改的便利性。5.4 问题撤销/重做功能异常关键点使用正确的API务必使用EditorUndoRedoManager而不是自己维护历史栈。操作对象撤销/重做的操作对象应该是整个ArrayMesh资源或者至少是完整的表面数据数组。避免只记录和恢复部分顶点数据容易造成状态不一致。操作边界一次完整的“笔触”从鼠标按下到释放应该作为一个独立的撤销动作。如果在一次笔触中提交了多次网格更新需要将它们合并到一个动作中否则用户需要按很多次CtrlZ才能撤销一笔。5.5 从“能用”到“好用”的进阶技巧笔刷预览在3D视口中鼠标位置实时显示一个半透明的球体或圆盘直观展示笔刷的大小和强度衰减范围。这需要你在_forward_3d_gui_input中在射线检测命中的位置动态生成或更新一个预览MeshInstance。材质过滤为你的地形模型添加一个自定义的EditorInspectorPlugin当在编辑器中选择该模型时自动在检查器中添加一个按钮“生成顶点颜色层”或“绑定绘画工具”提升工作流整合度。与地形系统集成如果你的项目使用了GridMap或自定义的地形系统需要将画笔工具的逻辑适配到这些系统的数据结构和更新机制上。核心思想不变找到存储“权重”的数据层并提供修改它的工具。保存与加载修改后的顶点颜色数据是存储在Mesh资源内部的。确保你的Mesh是唯一的不是多个实例共享的或者使用ResourceSaver将修改后的Mesh保存到独立的.mesh文件中避免影响原始资源。开发这样一个工具最难的不是着色器或绘制算法本身而是如何将其无缝、稳定、高效地整合到Godot的编辑器生态和你的项目管线中。它涉及到编辑器扩展、资源管理、性能优化和用户体验等多个方面。但一旦完成它带来的生产力提升和艺术表现力的解放绝对是值得的。
