Unity地形编辑器扩展实战:批处理、平滑、高度图导入导出与纹理统计
很多项目做到地形阶段都会卡在同一个问题上Unity自带的Terrain工具够用但不够顺手。美术团队想要一键统一所有地形切片的参数策划希望能把高度图导出去做地图流程程序则念念不忘一个可以精细控制的地形平滑算法。这些需求Unity默认编辑器都不直接提供于是“地形编辑器拓展功能类”就成了绕不开的活。这篇文章就围绕地形编辑器扩展功能类的完整开发过程来聊从编辑器扩展的基座设计开始到四个实用功能的具体实现多地形批处理、全局平滑、高度图导入导出、地表纹理统计。每个功能都包含完整思路、可落地的代码和实测中踩过的坑适合已经接触过Unity基础、想深入编辑器工具开发的开发者也适合技术美术参考。1. 为什么需要编辑器拓展地形工具的三个缺口先把话说透Unity内置的地形编辑器并不是不好用它的问题在于“单地形友好多地形痛苦批处理靠手点”。实际做开放世界或者大地图项目时场景里往往是几十块地形切片拼在一起。美术同学需要一块一块地设置分辨率、尺寸、材质、细节距离每换一次参数就要重复一整套点击。这种活本质上是机械重复人工做既慢又容易出错。另一个痛点是没有全局操作能力。内置的平滑笔刷非常好但它要求你按住鼠标在地形上涂抹。如果你想把整块地形的尖锐棱角全部平滑一遍或者只想处理某个高度范围内的区域内置工具就无能为力了。同样内置的地形高度数据无法方便地导出为外部图片格式程序想对接自己的地图工具链时只能自己想办法。第三个缺口是安全性。手动调整地形数据尤其在高分辨率地形上做大范围修改时一旦操作失误原来的地形可能就回不去了。Unity的Undo系统覆盖编辑器界面里的常规操作但它不一定会自动拦截你通过脚本直接调用TerrainData.SetHeights()这种底层修改。所以一套合格的地形拓展工具必须先解决“怎么改、改之前怎么备份、改错了怎么回滚”这组问题。我在自己的项目里就把这几点当成设计前提所有修改入口都包一层Undo所有批处理都带预览参数所有会覆盖原数据的操作都先导出备份。后面代码里也会体现这个思路。2. 工具的地基编辑器扩展类的基本结构和必备设施写地形扩展工具本质上就是写一个放在Editor文件夹下的编辑器脚本。它利用UnityEditor命名空间下的API在Unity的编辑器生命周期里注册菜单、绘制窗口、监听选择事件然后对你选中或场景里的地形对象执行批量操作。2.1 用一个静态工具类封装核心操作首先需要一个和UI解耦的核心工具类里面放纯粹的地形数据处理函数。这个类不依赖EditorWindow方便以后在测试环境或者自动化流程里复用。using System.Collections.Generic; using UnityEngine; public static class TerrainUtility { // 获取场景中所有的地形按名称排序保证结果稳定 public static ListTerrain GetAllTerrains() { var terrains new ListTerrain(); var all Object.FindObjectsOfTypeTerrain(); // 注意运行时脚本里不推荐FindObjectsOfType但编辑器工具可以接受 foreach (var t in all) { if (t ! null t.terrainData ! null) terrains.Add(t); } terrains.Sort((a, b) string.Compare(a.name, b.name, System.StringComparison.Ordinal)); return terrains; } // 统一修改地形尺寸和分辨率尽量保留已有高度 public static void ResizeTerrain(Terrain terrain, Vector2 size, int heightmapResolution, int baseMapResolution) { var data terrain.terrainData; Undo.RecordObject(data, Resize Terrain Data); var oldHeights data.GetHeights(0, 0, data.heightmapResolution, data.heightmapResolution); var oldSize data.size; var maxHeight data.size.y; data.heightmapResolution heightmapResolution; data.baseMapResolution baseMapResolution; data.size new Vector3(size.x, maxHeight, size.y); // 分辨率改变后高度数组需要重新映射这里先用拉伸方式近似保留 var newHeights ResampleHeightmap(oldHeights, heightmapResolution, heightmapResolution); data.SetHeights(0, 0, newHeights); EditorUtility.SetDirty(data); } private static float[,] ResampleHeightmap(float[,] oldHeights, int newWidth, int newHeight) { var oldW oldHeights.GetLength(0); var oldH oldHeights.GetLength(1); var result new float[newWidth, newHeight]; for (int y 0; y newHeight; y) { float srcY (float)y / (newHeight - 1) * (oldH - 1); int y0 Mathf.FloorToInt(srcY); int y1 Mathf.Min(y0 1, oldH - 1); float fy srcY - y0; for (int x 0; x newWidth; x) { float srcX (float)x / (newWidth - 1) * (oldW - 1); int x0 Mathf.FloorToInt(srcX); int x1 Mathf.Min(x0 1, oldW - 1); float fx srcX - x0; float v00 oldHeights[y0, x0]; float v10 oldHeights[y0, x1]; float v01 oldHeights[y1, x0]; float v11 oldHeights[y1, x1]; float v Mathf.Lerp( Mathf.Lerp(v00, v10, fx), Mathf.Lerp(v01, v11, fx), fy); result[y, x] v; } } return result; } }这段代码里有两个细节值得注意。第一GetHeights和SetHeights操作的是高度图分辨率矩阵不是size里的世界单位所以重采样时必须基于heightmapResolution的原始尺寸来做。第二直接改size的Y值会导致地形整体被拉伸或压缩大部分情况下我们希望保留原有最大高度所以代码里先存了maxHeight再恢复。2.2 用EditorWindow承载可调参数的入口有了核心工具类接下来需要一个编辑器窗口不然每次调参数都要改代码没法给美术直接使用。标准的做法是继承EditorWindow在OnGUI里用EditorGUILayout画参数用GUILayout.Button触发操作。using UnityEditor; using UnityEngine; using System.Collections.Generic; public class TerrainToolboxWindow : EditorWindow { private Vector2 terrainTileSize new Vector2(250, 250); private int heightmapResolution 513; private int baseMapResolution 1024; private float smoothStrength 0.5f; private int smoothIterations 1; private bool onlySmoothSelected true; private string exportPath Assets/ExportedTerrain; [MenuItem(Tools/Terrain Toolbox)] public static void Open() { var window GetWindowTerrainToolboxWindow(Terrain Toolbox); window.minSize new Vector2(360, 460); window.Show(); } private void OnEnable() { // 监听选中变化刷新窗口里的提示信息 Selection.selectionChanged Repaint; } private void OnDisable() { Selection.selectionChanged - Repaint; } private void OnGUI() { EditorGUILayout.LabelField(地形批量设置, EditorStyles.boldLabel); terrainTileSize EditorGUILayout.Vector2Field(地形尺寸, terrainTileSize); heightmapResolution EditorGUILayout.IntField(高度图分辨率, heightmapResolution); baseMapResolution EditorGUILayout.IntField(控制贴图分辨率, baseMapResolution); if (GUILayout.Button(应用到所有地形)) { var terrains TerrainUtility.GetAllTerrains(); foreach (var t in terrains) TerrainUtility.ResizeTerrain(t, terrainTileSize, heightmapResolution, baseMapResolution); EditorUtility.DisplayDialog(完成, $已修改 {terrains.Count} 个地形, OK); } EditorGUILayout.Space(16); EditorGUILayout.LabelField(全局平滑, EditorStyles.boldLabel); smoothStrength EditorGUILayout.Slider(平滑强度, smoothStrength, 0f, 1f); smoothIterations EditorGUILayout.IntField(迭代次数, smoothIterations); onlySmoothSelected EditorGUILayout.Toggle(仅平滑选中地形, onlySmoothSelected); // 这里只是示例实际平滑函数在下一节实现 if (GUILayout.Button(执行平滑)) { Debug.Log(平滑功能在3.2节实现); } } }这个骨架解决的是“一个每个项目都会遇到的工程问题”工具窗口如何组织、参数如何持久化、操作如何触发。示例代码没有做参数持久化如果你希望关闭窗口再打开后保留上次的数值可以用EditorPrefs.SetFloat和EditorPrefs.GetFloat保存这几个字段的值我实际项目中是直接用ScriptableObject存档的因为参数量大了以后EditorPrefs会变得很乱。2.3 数据安全与Undo的底层逻辑编辑器工具最怕的不是功能不强大而是“一键毁全图”。Unity的序列化系统非常优秀但它不会自动为你脚本里的每一次修改注册撤销节点。你需要明确调用Undo.RecordObject(target, 操作名称)或者在批量修改完后调用EditorUtility.SetDirty(target)通知Unity资源系统保存。实际操作时我的做法是修改地形数据前Undo.RecordObject(t.terrainData, 批量地形修改)修改地形GameObject自身比如位置、缩放Undo.RecordObject(t.transform, 批量地形位置修改)修改Terrain组件上的参数如detailDistanceUndo.RecordObject(t, 批量地形组件参数修改)只要把这步做在前面后续即使出了开发事故美术也能用CtrlZ一次回滚所有地形的修改。不要在Undo上偷懒一次批量操作影响几十个地形数据时没有Undo的后果能让整个美术组崩溃。3. 核心功能实现四个能直接落地的拓展工具基础框架搭好了现在开始填充真正有业务价值的四个功能模块。这四个功能我都实际放到项目里跑过下面展示的代码是精简掉项目特定业务逻辑之后的通用版本。3.1 多地形批处理属性统一与自动配置大地图项目里的地形切片通常由不同美术负责最后合到一起时经常出现分辨率不一致、尺寸偏差、TerrainLayer顺序混乱的问题。手动统一非常痛苦批处理工具正好派上用场。除了前面TerrainUtility里的尺寸和分辨率修改实际项目里还需要自动设置地形的位置排布。比如一个4块地形的场景拼成正方形第0块在左下角第1块在右下角第2块在左上角第3块在右上角。这个排布逻辑看起来简单但手动操作很容易出现缝隙。public static void AlignTerrains(ListTerrain terrains, Vector2 tileSize, int gridWidth) { for (int i 0; i terrains.Count; i) { var t terrains[i]; int x i % gridWidth; int z i / gridWidth; Undo.RecordObject(t.transform, Align Terrains); var pos t.transform.position; pos.x x * tileSize.x; pos.z z * tileSize.y; t.transform.position pos; } }这里面的顺序取决于美术组的命名约定。我通常建议把“按名称排序 按行列排布”做成下拉框选项因为不同项目的场景组织方式差异很大。记得设置地形时同时处理TerrainCollider有时候地形位置改了碰撞体还停留在原处角色就会穿模。批处理工具的另一个隐藏价值是生成检查报告。我的工具在每次执行完后会打印一行摘要Debug.Log($[TerrainToolbox] {terrains.Count} 个地形已对齐行 {rows}列 {cols}间隙检测通过);这一步成本很低但能在团队协作时快速告诉所有人“你刚才的操作是否有意外影响”。我遇到过美术误操作后要找出是哪个地形被改过的场景有摘要日志后排查时间从半天缩短到了十分钟。3.2 全局地形平滑可控的邻域平均算法Unity内置的平滑笔刷只支持局部涂抹而且它的算法是黑盒的。我们需要一个可以作用于整块地形、带有强度控制和迭代次数的全局平滑函数。高度图平滑有多种实现方案最直观的是邻域平均。对高度图的每个点取它周围8个格子或上下左右4个格子的平均值然后把平滑后的值按强度比例混合回原值。这种做法简单稳定缺点是会把悬崖峭壁和平坦地面一视同仁地“抹匀”真实感会有所下降。所以我在项目里加了坡度保护根据当前点和周围点的最大高度差动态降低混合强度。差值越大表示此处越可能是陡峭地形就越不参与平滑。这个改动虽然只是加了一个因子但效果差别非常大能保证山脉的棱线不会被一个全局平滑彻底抹成土丘。public static void SmoothTerrain(Terrain terrain, float strength, int iterations) { var data terrain.terrainData; int res data.heightmapResolution; float[,] heights data.GetHeights(0, 0, res, res); // 高度差阈值单位是高度图比例值不是世界单位 float sharpnessLimit 0.05f; for (int iter 0; iter iterations; iter) { float[,] smoothed new float[res, res]; for (int y 1; y res - 1; y) { for (int x 1; x res - 1; x) { float center heights[y, x]; float left heights[y, x - 1]; float right heights[y, x 1]; float down heights[y - 1, x]; float up heights[y 1, x]; float maxDiff Mathf.Max( Mathf.Abs(center - left), Mathf.Abs(center - right), Mathf.Abs(center - down), Mathf.Abs(center - up) ); float neighborAvg (left right down up) * 0.25f; float t strength; // 防止过渡平滑陡峭区域 if (maxDiff sharpnessLimit) { float fade Mathf.Clamp01(1f - (maxDiff - sharpnessLimit) / 0.1f); t * fade; } smoothed[y, x] Mathf.Lerp(center, neighborAvg, t); } } // 把边缘复制过来边缘不参与平滑但也不能丢失 for (int x 0; x res; x) { smoothed[0, x] heights[0, x]; smoothed[res - 1, x] heights[res - 1, x]; } for (int y 0; y res; y) { smoothed[y, 0] heights[y, 0]; smoothed[y, res - 1] heights[y, res - 1]; } for (int y 0; y res; y) for (int x 0; x res; x) heights[y, x] smoothed[y, x]; } Undo.RecordObject(data, Smooth Terrain); data.SetHeights(0, 0, heights); EditorUtility.SetDirty(data); }有几个性能点要特别说明。GetHeights(0, 0, res, res)在高度图分辨率是513x513时返回的float[,]数组大约占用1MB内存每个float 4字节5135134约1MB看上去不大但在迭代循环里反复创建新数组会有明显GC分配。更好的做法是复用数组或者把平滑主体放到一个临时数组里迭代完再整体写回。如果你要处理的分辨率是2049甚至4097数组会膨胀到16MB到64MB迭代过程中还伴随大量算术运算建议用Unity的Job System或者直接用NativeArray重写。我团队里的工具在4097分辨率下做10次平滑开启了Jobs之后耗时从原来的十几秒降到了两秒以内这个优化幅度非常值得。我的建议是不要在高分辨率地形上追求一次平滑的完美效果可以把平滑迭代次数设小然后多次执行。每次执行之间去检查地形边缘是否出现了接缝可以有效避免“平滑过头导致地形边缘悬空”的情况。3.3 高度图导入导出打通程序化地图工具链地形高度图导出是需求频率非常高的功能。程序那边需要把地形的真实高度和坡度数据交给路径规划算法或者想用外部工具生成地形再倒回Unity。Unity默认没有这个功能只有非常原始的RawFormat导出。自己写一个PNG导出器并不复杂核心是把TerrainData.GetHeights返回的二维数组编码进Texture2D再调用EncodeToPNG。我选择用16位的R16纹理格式来保存高度信息。用8位的话256个色阶对于高度图来说精度太低地形高度是256米时每一阶代表1米起伏完全丢失。16位能提供65536个色阶在大多数游戏场景中已经把误差控制在了毫米级别。public static void ExportHeightmapPNG(Terrain terrain, string filePath) { var data terrain.terrainData; int res data.heightmapResolution; float maxHeight data.size.y; float[,] heights data.GetHeights(0, 0, res, res); // R16格式在Texture2D中不能直接EncodeToPNG这里用RGBAFloat承载并手动转换 Texture2D tex new Texture2D(res, res, TextureFormat.RGBAFloat, false); for (int y 0; y res; y) { for (int x 0; x res; x) { float h Mathf.Clamp01(heights[y, x]); float pixel h * 65535f; ushort encoded (ushort)Mathf.RoundToInt(pixel); // 将ushort拆成两个字节放进RG通道 tex.SetPixel(x, y, new Color( (encoded 8) / 255f, (encoded 0xFF) / 255f, 0f, 1f)); } } tex.Apply(); byte[] pngBytes tex.EncodeToPNG(); System.IO.File.WriteAllBytes(filePath, pngBytes); DestroyImmediate(tex); AssetDatabase.Refresh(); }对应地导入函数要反过来解析两个字节并合并成一个ushort再除以65535还原成0到1的高度比例。这一步里最容易出错的是高度数据的方向。Unity的高度图数组索引是从左下角开始的从左下到右上排列。PNG图片的像素坐标是从左上角开始。如果直接导出再导入地形会上下翻转。处理办法是导出时把数组在Y方向翻转一遍或者在导入解析时翻转。具体翻转哪一边不重要重要的是导出和导入使用一致的规则。我通常规定所有地图数据文件一律以左下角为原点PNG文件在转换为高度数据时第一行对应图片底部这样符合Unity内部约定。导出功能还有几个常见变体比如同时导出坡度图。坡度图不是直接能用的PNG它是一个灰度图像素亮度表示地形在当前位置的倾斜程度。做法是用高度图数组计算每个点与周围点的平均梯度差然后转成0到1的灰度值。美术可以拿这张图做纹理叠加的Mask程序可以拿它做寻路时的移动花费惩罚。我做的工具里把高度图导出和坡度图导出放在了同一个面板一键生成两个文件。3.4 地表纹理统计数据化地检查地形外观最后一个功能是对地形上的地表纹理做统计。纯功能角度它没有前几个那么刚需但实际用起来很能提升团队协作效率。美术想知道当前地形上草地、岩石、沙地各自占据多少比例以及有没有哪个区域忘记铺设纹理导致露出默认材质。靠肉眼检查几十片地形切片非常不现实写一个统计工具就快多了。统计原理也不复杂。每个地形切片都有terrainData.terrainLayers存放纹理层数组而每个网格顶点会有一个权重值splat map表示不同纹理在这个位置的混合比例。我们要做的就是把所有顶点的权重数组累加计算出每种纹理占所有有效区域的比例。注意splat map的尺寸是alphamapResolution和高度图分辨率不一定相同。读取时用terrainData.GetAlphamaps(0, 0, alphaWidth, alphaHeight)。public static DictionaryTerrainLayer, float CalculateTextureCoverage(Terrain terrain) { var data terrain.terrainData; var layers data.terrainLayers; var result new DictionaryTerrainLayer, float(); if (layers null || layers.Length 0) return result; int width data.alphamapWidth; int height data.alphamapHeight; float[,,] maps data.GetAlphamaps(0, 0, width, height); float[] sums new float[layers.Length]; float total 0f; for (int y 0; y height; y) { for (int x 0; x width; x) { // 最多的纹理层权重可能不止一层有值累加所有层 for (int l 0; l layers.Length; l) { sums[l] maps[x, y, l]; // 注意GetAlphamaps返回的数组索引顺序是[x, y, layer] } total 1f; } } for (int l 0; l layers.Length; l) result[layers[l]] sums[l] / Mathf.Max(total, 0.0001f); return result; }这里有个新手容易踩的坑GetAlphamaps返回的三维数组前两维的顺序不是[y, x, layer]而是[x, y, layer]。如果你按照读二维图片的习惯写maps[y, x]取到的数据是转置的统计结果整体没问题但如果你要做区域选点或者微调某个坐标下的权重就会错位。我把统计结果输出成两种形式编辑器Console里打印一行摘要以及一个临时的SimpleTreeView展示每层纹理的占比条。项目里美术组最终依赖的是第二种因为百分比数字对人眼来说不如进度条直观。实现上其实很简单在窗口的OnGUI里用EditorGUILayout.HelpBox画一个带背景色的矩形即可。这个工具的价值不在“做完这个功能”而在后续迭代中。比如地编同学调整了某个地形的纹理权重后只要点一下统计就能立刻看到地表覆盖率的变化是否合理还能自动检查是否有某一块地区的权重和突然变成0快速定位到漏铺贴图的地方。4. 实测踩坑记录编辑器扩展开发的常见问题与排查思路地形编辑器扩展这类工具坑通常不在功能逻辑而在Unity编辑器API的使用边界。我把实际遇到过的、有一定代表性的问题整理成了表格式的速查手册相信能帮你节省很多调试时间。现象根因解决方案SetHeights后地形没有立即变化修改了TerrainData但没有标记脏数据或者场景视图没重绘调用EditorUtility.SetDirty(data)和SceneView.RepaintAll()CtrlZ撤不掉工具的批量修改没有在操作前调用Undo.RecordObject在修改TerrainData、Terrain组件、Transform前分别记录窗口打开时Selection不为空但按钮不生效事件流被吞掉或者按钮在OnGUI中不可点击区域检查GUILayout.Button是否放在EditorGUILayout.BeginVertical内按钮逻辑里先Event.current.Use()高分辨率地形批量平滑卡死编辑器高度图数组16MB迭代循环使用纯C#主线程阻塞降低迭代次数改用Jobs或者做分块并行处理导出的PNG导入后地形上下颠倒Unity高度数组与PNG像素坐标的Y方向不一致统一约定导出时数组行坐标翻转或导入解析时翻转地形纹理统计结果和肉眼观察差异大splat map的混合权重是平滑过渡的边缘区域会有大量半权重值统计时加阈值比如单层权重大于0.5才计入该类型批处理工具在某些机器上无法注册菜单脚本没有放在Editor文件夹下确认脚本路径在Assets/Editor/或Assets/某目录/Editor/内除了表格里这些还有一个很容易被忽略的编辑器生命周期问题。EditorWindow.OnGUI在非交互时也会被频繁调用如果每次重绘都调用Selection.activeTerrain去刷新数据结构窗口会有明显卡顿。正确的做法是把计算量大的逻辑放到按钮事件里把轻量的显示逻辑放进OnGUI。我的经验是大计算一律延迟用EditorApplication.delayCall把耗时方法丢到下一帧执行避免在同一个OnGUI流程里阻塞绘制。再聊一个接口兼容性问题Unity在2019.2之后引入了新的TerrainAPI比如TerrainPaintUtility。如果你的项目用的Unity版本比较新还有一套更底层的PaintContext机制可以用它允许自定义笔刷直接操作高度或者纹理效果比旧版的SetHeights流畅得多。但代价是学习成本更高API也更底层。我的建议是对于批处理和全局操作依然用TerrainData这套旧API简单稳定对于做自由笔刷类扩展再去学习TerrainPaintUtility。不要一上来就上底层API除非你要做的确实是笔刷工具。地形文件本身也有坑。Unity的TerrainData本质上是一个资产文件当你修改这个文件时所有引用这个TerrainData的地形都会同步改变。团队用SVN或Git协作时一个地形资产往往对应多个Prefab引用批处理工具在改TerrainData之前最好先确认当前场景里的地形引用的是独立的TerrainData还是共享了某个公共资产。共享资产的情况下批量修改会影响到所有使用该资产的地形实例这种情况往往是好的比如统一整个世界的分辨率但如果在做局部调整就会引发“为什么别的关卡也变了”的问题。工具里最好加入Inspector显示当前TerrainData被多少个Terrain对象引用或者在修改前弹出确认框。5. 进阶思路从“能用”到“好用”的编辑器工具设计心得聊到这一步地形编辑器拓展类的核心功能已经完整了。但如果你想把它从“自己用的脚本”升级成“团队持续使用的工具”还有几个方向值得投入。第一个方向是增加参数预设系统。所有工具参数都能保存成预设文件比如“野外地图_高精度”预设包含高度图分辨率2049、纹理清晰度1024、全局平滑强度0.3而“低配移动端”预设则把分辨率降到513、纹理清晰度256。预设用ScriptableObject存储放在Assets/Editor/Presets目录下用下拉框切换。这样美术在切换项目阶段时只需要选一个预设工具就会自动填好所有参数。第二个方向是自定义笔刷系列拓展。批处理和全局平滑是覆盖整个地形的操作但美术经常只需要处理某个区域。这时可以基于TerrainPaintUtility制作一个带Mask贴图的自定义高度笔刷。Mask可以用美术自己画的灰度图也可以直接用地形上已有的纹理混合权重当作Mask。实现思路跟普通的PaintContext差不多区别是你在修改Heightmap前先把BrushTransform和PaintContext准备好。这个功能做起来比表面复杂一旦做出来会变成美术最常用的工具。第三个方向是地形数据校验器。我见过很多项目在合并地编分支时出现地形间有微小的高差缝隙原因通常不是地形位置错位而是相邻地形的高度图边缘像素不是严格一致。写一个校验工具读取相邻两块地形的边界高度值计算差值超过阈值就高亮标红。这个工具代码量不大但能在开发中后期省下巨量的美术排查时间。我自己在使用中还有一个小习惯每次工具执行完都自动生成一份带时间戳的处理日志内容包括操作类型、涉及地形数量、主要参数、执行耗时、是否成功。这个日志不要求很复杂用Debug.Log加上\n分隔信息就够了。有这份日志出问题时的回溯效率极高特别是团队里多人同时操作同一个大地图工程的时候。地形编辑器拓展的价值往大了说是在优化整个团队的工作流往小了说是在帮你从重复劳动里解脱出来。做工具这件事最有趣的地方也在这里每解决一类痛点后面就能省出一大块时间用来做真正有创造性的工作。目前这套工具类在我这边已经演变成了一个比较完整的地形工作流支撑层今后如果继续迭代我大概率会往TerrainLayer自动归类、地形细节自动预算这些方向扩展让美术把精力集中在真正需要审美判断的事情上。

相关新闻

从ThinkPHP到Laravel:校园点歌音乐网站系统设计与实现

从ThinkPHP到Laravel:校园点歌音乐网站系统设计与实现

看到“基于Thinkphp-Laravel畅听校园点歌音乐网站系统的设计与实现”这个标题,很多人的第一反应是:这到底是用ThinkPHP还是用Laravel?两个框架能同时出现在一个项目里?这还真不是笔误。我在实际项目里把同样的业务用ThinkPHP先做了…

2026/10/14 7:35:40 阅读更多 →
AnyPS5串流方案全解析:从架构设计到实操调优的完整指南

AnyPS5串流方案全解析:从架构设计到实操调优的完整指南

1. 从“AnyPS5”这个标题说起:它到底想解决什么问题第一次看到“AnyPS5”这个标题,我脑子里蹦出来的第一个念头是:这大概率不是一个官方项目,而是一个民间开发者或者小团队搞出来的“跨设备串流/远程使用”方案。为什么这么判断&a…

2026/10/14 7:35:40 阅读更多 →
桌面式真空回流焊在中小电子厂的实际使用复盘

桌面式真空回流焊在中小电子厂的实际使用复盘

去年夏天,我跟着一位在电子装联行业摸爬滚打了十几年的工艺主管老周,去了他们位于龙岗的厂区。厂不大,两条SMT线,一条插件后焊线,主要做安防摄像头的主板代工。老周拉我进实验室,指着一台桌面式真空回流焊说…

2026/10/14 7:35:40 阅读更多 →

最新新闻

Natron ErodeSmooth 节点完全指南:归一化功率加权腐蚀/膨胀滤镜的参数解析与源码级解读

Natron ErodeSmooth 节点完全指南:归一化功率加权腐蚀/膨胀滤镜的参数解析与源码级解读

音视频视频处理图形学桌面应用 【免费下载链接】Natron Open-source video compositing software. Node-graph based. Similar in functionalities to Adobe After Effects and Nuke by The Foundry. 项目地址: https://gitcode.com/gh_mirrors/na/Natron 点击查看 …

2026/10/15 3:32:41 阅读更多 →
GreaterWMS 移动端构建实战:基于 Quasar CLI 与 Cordova 的 Android 打包安装全流程

GreaterWMS 移动端构建实战:基于 Quasar CLI 与 Cordova 的 Android 打包安装全流程

后端前端企业应用 【免费下载链接】GreaterWMS This Inventory management system is the currently Ford Asia Pacific after-sales logistics warehousing supply chain process . After I leave Ford , I start this project . You can share your vacant warehouse space, …

2026/10/15 3:32:41 阅读更多 →
从Apollo-11代号项目到可复现工程:逆向拆解与改造指南

从Apollo-11代号项目到可复现工程:逆向拆解与改造指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 3:32:41 阅读更多 →
rospack depends1 报错排查:ROS依赖查询与环境变量配置实战

rospack depends1 报错排查:ROS依赖查询与环境变量配置实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 3:32:41 阅读更多 →
Vue2多页面应用模板:从vue.config.js到webpack配置的完整实践

Vue2多页面应用模板:从vue.config.js到webpack配置的完整实践

简介:Vue 2.0多页面应用模板面向需要同时维护多个独立页面的前端开发场景,旨在解决传统单页应用在官网、专题页等多入口项目中重复搭建构建流程、难以统一工程化配置的问题。资源包共32个文件,压缩后仅31KB,包含15个JavaScript脚本…

2026/10/15 3:32:41 阅读更多 →
NC65销售订单批量导入实战:从数据清洗到SQL脚本全攻略

NC65销售订单批量导入实战:从数据清洗到SQL脚本全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 3:31:40 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →