使用Unity Terrain API批量刷草
解读
面试官问“批量刷草”并不是让你背一句“TerrainData.SetDetailLayer”,而是想看你在真实项目里能不能把“草”当成资源+性能+工具链的一环系统解决。国内项目普遍要跑中低端安卓+iOS,草密度一上来就掉帧,策划还天天改范围、改密度、改品种;美术给的贴图不是2的幂就是带透明锯齿;热更新还要保证草不丢。所以回答必须体现:
- 对TerrainData.DetailResolution与PatchCount的底层理解;
- 能写编辑器批处理工具让策划一键刷;
- 知道GPU Instancing + Density Map才是线上性能底线;
- 熟悉分包、AssetBundle、热更后草的恢复逻辑。
一句话:把“刷草”答成“性能+工具+管线”才配拿U3D高薪。
知识点
- TerrainData.SetDetailLayer(int xBase, int yBase, int layer, int[,] map):唯一写入接口,必须在Undo.RecordObject包裹下使用,否则编辑器撤销栈会崩。
- DetailResolution、DetailResolutionPerPatch、PatchCount三者的耦合关系:Resolution必须是PatchCount的整数倍,否则Unity自动对齐,导致刷出来的草错位。
- Grass Tint vs Healthy/Dry Color:移动端千万不要用Terrain自带的Tint纹理,会额外一次DrawCall;直接让美术把颜色做进Albedo。
- GPU Instancing + Density Map:Unity 2021以后草支持DrawMeshInstancedIndirect,把Density Map做成R8贴图,采样后discard掉alpha<0.3的片,同等密度下DC从800降到2。
- 热更恢复:TerrainData不标记为Addressable,重启后草丢失;必须在RuntimeInitializeOnLoad里重新执行SetDetailLayer,数据来自ScriptableObject或远程Json。
- 编辑器批处理:用TerrainTools API(Unity.TerrainTools)里的PaintContext.ReadPixels+WritePixels,可以一次读写多层DetailLayer,比逐像素Set快40倍。
答案
分三步给面试官:工具、运行时、性能兜底。
- 编辑器批处理工具
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using UnityEngine.TerrainTools;
public static class GrassPainter
{
[MenuItem("Tools/批量刷草")]
static void Paint()
{
Terrain terrain = Selection.activeObject as Terrain;
if (!terrain) return;
TerrainData data = terrain.terrainData;
Undo.RecordObject(data, "PaintGrass");
int resolution = data.detailResolution;
int[,] layer = new int[resolution, resolution];
// 按策划配置的密度曲线采样
AnimationCurve density = AssetDatabase.LoadAssetAtPath<AnimationCurve>("Assets/Config/grassDensity.asset");
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
float worldX = x / (float)resolution * data.size.x;
float worldZ = y / (float)resolution * data.size.z;
float noise = Mathf.PerlinNoise(worldX * 0.1f, worldZ * 0.1f);
layer[y, x] = Mathf.RoundToInt(density.Evaluate(noise) * 8);
}
}
data.SetDetailLayer(0, 0, 0, layer);
EditorUtility.SetDirty(data);
}
}
#endif
关键点:用Undo.RecordObject保证可撤销;density曲线让策划调数值不碰代码。
- 运行时热更恢复
[System.Serializable]
public class GrassChunk
{
public int xBase, yBase;
public int[] flattened; // 扁平化后的int[]
}
public class GrassSync : MonoBehaviour
{
void Start()
{
TerrainData data = Terrain.activeTerrain.terrainData;
string json = Addressables.LoadAssetAsync<TextAsset>("grassChunk.json").WaitForCompletion().text;
GrassChunk chunk = JsonUtility.FromJson<GrassChunk>(json);
int[,] map = new int[data.detailResolution, data.detailResolution];
Buffer.BlockCopy(chunk.flattened, 0, map, 0, chunk.flattened.Length * sizeof(int));
data.SetDetailLayer(chunk.xBase, chunk.yBase, 0, map);
}
}
关键点:Addressables保证分包;Buffer.BlockCopy把扁平数组还原为2维,比双层for快10倍。
- 性能兜底
- 打开Edit→Project Settings→Graphics→Instancing Variants,把草用到的Shader加到Always Included;
- 在Quality Settings里把Detail Distance调到80m,Fade Length 20m,低端机再降50%;
- 如果草品种>4,合并贴图Atlas,用GPU Instancing一次性画,DC<=2;
- 安卓低端机用R8 Density Map替代Terrain草,Shader里discard,同等密度帧率提升35%。
拓展思考
- 大世界分块:Terrain的DetailLayer是全图内存,4096×4096分辨率直接占256MB。国内开放世界项目用Virtual Texture+自定义Sparse Clip Map,把草拆成512×512块,只加载玩家周围3×3块,内存降到1/9。
- SRP Batch兼容性:URP下Terrain草默认走Forward,DC爆炸。可以改写成Shader Graph的Unlit Master,勾选GPU Instancing,再手动加_CameraDepthTexture采样做Soft Intersection,DC压到1。
- 风场交互:国内竞品《逆水寒》手游用Compute Shader每帧更新StructuredBuffer<float2> windOffset,草顶点Shader里采样,CPU 0开销;Unity 2022的DOTS Instancing也能直接喂Custom Buffer,同样思路。
- 策划黑盒:把密度、高度、颜色三曲线做成ScriptableObject,打AssetBundle;上线后策划只换Bundle就能热更草皮,无需客户端发包,国内买量包体敏感环境刚需。