之前这个博客:
https://blog.csdn.net/wodownload2/article/details/90263882
主要是关于如何把场景中的物件,进行空间划分,并且根据主角的位置,动态的加载周边的物件,对于大地形的处理,则没有涉及到。
经过搜索,主要收集了如下具有参考价值的网址:
重点参考三个网址:
http://www.cnblogs.com/jietian331/p/5831062.html 本文重点讲述
https://blog.csdn.net/zr339361504/article/details/53352800 这个文章涉及法线的计算
https://gitee.com/langresser_king/terrain_proj 码云有代码,分割地形的方法同上面一个网址
https://catlikecoding.com/unity/tutorials/procedural-grid/ 一个很好的关于unity学习的网址,后面会重点研究和这个博客
http://www.52vr.com/article-1173-1.html 无限大地形生成
http://c.biancheng.net/view/2739.html
https://www.bilibili.com/video/av4380122?from=search&seid=13314329861953277930 视频教程高度图生成地形
http://darrellbircsak.com/2017/01/27/split-unity-terrain-script/ 分割地形的blog
https://kostiantyn-dvornik.blogspot.com/2013/12/unity-split-terrain-script.html
http://indago.homenko.pl/wp-content/uploads/2016/08/World-Streamer-Manual.pdf
https://unity3d.com/how-to/big-games-on-low-end-mobile
https://www.gamasutra.com/blogs/ChristophEnder/20170222/292179/Open_World_on_Mobile_with_Unity.php
https://mattgadient.com/2014/09/28/unity3d-a-free-script-to-convert-a-splatmap-to-a-png/
https://github.com/tangrams/unity-terrain-example
https://docs.unity3d.com/ScriptReference/TerrainData.GetAlphamaps.html
https://www.jianshu.com/p/264e9665f6b1
https://www.bbsmax.com/A/amd0624LJg/
https://connect.unity.com/p/mte-mesh-terrain-editor-mo-xing-di-xing-bian-ji-qi
https://www.bilibili.com/video/av10191087/
http://www.manew.com/thread-20801-1-1.html mesh terrain editor插件下载
https://www.bilibili.com/video/av10191087/?p=1 bili教程
http://new-play.tudou.com/v/XMjQ1MTY5MTY4MA==.html 土豆mte教程
http://www.vr2.tv/develop/unity-chajian-kaifa-jieshao.html 十款unity必备插件
https://gameinstitute.qq.com/community/detail/126192 lam地编工具
https://github.com/wachel/TerrainToLodMesh github 将terrain转换mesh
https://github.com/jinsek/MightyTerrainMesh 四叉树加载mesh github源码
https://zhuanlan.zhihu.com/p/64809281 上面的知乎博客
https://zhuanlan.zhihu.com/p/53355843 知乎高度图生成terrain
https://www.cnblogs.com/AZ-ZK/p/4219981.html 感觉是个大牛
http://gulu-dev.com/ 感觉是个大牛
http://oldking.wang/ 隔壁老王的博客
关于插件:
t4m 过时
mte 只有dll
terrain to mesh 只有dll
上面的这几个也曾视图去分析是否源码,然后进行分析,定制自己的unity terrain转mesh的需求,但是未果,所以还是重点参考了两篇文章:
http://www.cnblogs.com/jietian331/p/5831062.html 本文重点讲述
https://gitee.com/langresser_king/terrain_proj 码云有代码,分割地形的方法同上面一个网址
ok,下面来重点分析下其实现的过程。
1、首先是把unity的地形转为mesh
要明白为啥这么做?
引用https://zhuanlan.zhihu.com/p/64809281的一段文字:
Unity的Terrain一直被移动开发团队诟病其可用性,使用原生的Terrain的移动项目大部分都是平地,仅使用了贴图混合的部分。对于有高低起伏的Terrain采用转为mesh的方式使用,早期常用的插件如T4M和Terrain2Mesh,在Unity2018对terrain做了改动以后貌似都不再更新了。T4M更倾向于一个方便Artist修改模型的工具,不过一个模型反反复复地在多个工具中来回实在是很痛苦。Terrain2Mesh使用的人不多,本身比较简单,仅能导出固定规则网格和材质,限制也比较多。
归结为一句话就是性能,unity提供的terrain性能较差。再加上如果是特大地形就必须分开加载,综上,需要将terrain转换为mesh,然后进行分块处理。
下面就是代码的部分,首先是进行terrain转mesh:
[MenuItem("Terrain/Convert terrain to mesh")]
static void Init()
{
//1.所选择的物体是否为空,在Hierarchy视图选中一个物体即可。
if (Selection.objects.Length <= 0)
{
Debug.Log("Selection.objects.Length <= 0");
return;
}
var terrainObj = Selection.objects[0] as GameObject;
if (terrainObj == null)
{
Debug.Log("terrainObj == null");
return;
}
//2.获取地形选中物体身上的组件Terrain
var terrain = terrainObj.GetComponent<Terrain>();
if (terrain == null)
{
Debug.Log("terrain == null");
return;
}
//3.判断是否有地形数据
var terrainData = terrain.terrainData;
if (terrainData == null)
{
Debug.Log("terrainData == null");
return;
}
下面看看unity给我们提供的地形的样子:
int vertexCountScale = 4;
int w = terrainData.heightmapWidth; //高度图的宽度
int h = terrainData.heightmapHeight;//高度图的高度
Vector3 size = terrainData.size; //地形的长宽高
这里很多人会对接下来的代码产生怀疑,我也不例外,但是经过逐行的理解,还是参悟了其中的道理。
首先是vertexCountScale的意义,其实就是距离多远采样一个点,也就是采样的精度。
比如地形原先是10001000大小:
那么如果按照距离4进行采样,最后得出的就是250250个点。
高度图的宽和高,以及地形的长宽高,在配置中可以看出来:
这里提下Heightmap Resolution为啥是513:
float[,,] alphaMapData = terrainData.GetAlphamaps(0, 0, terrainData.alphamapWidth, terrainData.alphamapHeight);
这个函数的意思是,返回从(0,0)开始,到(terrainData.alphamapWidth, terrainData.alphamapHeight)宽度和高度的三维数组。
float[,]第一维和第二维构成了对应(x,y)坐标;第三维是对应SplatAlpha贴图的哪个通道,而float[,]的值,是对应通道的值。
比如我们自己创建的地形使用到两个贴图,那么只有一个splatalpha,unity使用RGBA四个通道值,对应四个贴图的混合度。如下图所示你可能看得更明白一点:
这里使用两个贴图,所以根绝RGBA四个通道,只使用了RG通道。
第一个贴图对应的全是红色;第二个贴图对应的全是绿色。
如果有人问,如果是大于4个贴图怎么办,是的,会形成多个splatalpha贴图,这里只考虑最多四个贴图的情况。
我们可以使用如下的代码测试:
float aaaa = alphaMapData[0, 0, 0]; //第一个贴图的alpha值
float bbbb = alphaMapData[0, 0, 1]; //第二个贴图的alpha值
Vector3 meshScale = new Vector3(size.x / (w - 1f) * vertexCountScale, 1, size.z / (h - 1f) * vertexCountScale);
有人可能不太明白这个代码的意思,下面我就来具体的阐述一下:
如果要看懂的话,还需仔细阅下面的代码。
首先要确定点的x和y,然后确定z。那么到底要生成多少个点呢?
我们是根据高度图来生成mesh网格的。高度图的意思是对应(x,y)处的高度是多少。
有人就问了,难道地形是10001000,高度图不是10001000吗?可以是也可以不是,他们没有必然相等的要求,高度图可以低分辨率,比如上面的513,其实是512分辨率。
我的理解是地形是10001000,但是并不是所有的点都能对应一个高度图上的一个点,可以是多个点对应高度图上的同一个点。
下面我们就要理解下地形的长宽的意义了:
我们把地形设置为111的大小,那么它和111的Quad是什么大小关系:
可以看到其实11*1的地形就是一个Quad大小。
下面我们就来确定下,到底要生成多少个点,以谁为标准生成点。
由于我们要确定点的高度,也就是y轴坐标,所以我们必须以高度图为标准生成点。那么我们就要在高度图上每4个点采样一个点,然后得到对应的height,组成(x,y,z)一个点。
那么有人会问了,如果地形是10241024,而高度图是512512,那么以高度图为标准,也就是生成了512/4=128
128128个点,也就是128128的地形,那么这个缩小了呀,本来是1024*1024大小的地形,这样就缩小了。
所以要有一个缩放的关系:
所以要求出本来地形的宽度和高度图宽度的几倍,然后再乘以每隔多少采样一个点,这样求出一个缩放系数,最后求出的点再乘以和这个系数,就恢复到了原来地形的尺寸了。
比如上面:地形10241024
高度图是:512512
所以1024/512=2
再者,每个4个单位取高度图,所以2*4=8
这样求出坐标之后,再乘以8即可。
代码如下:
int vertexCountScale = 4;
int w = terrainData.heightmapWidth; //高度图的宽度
int h = terrainData.heightmapHeight;//高度图的高度
Vector3 size = terrainData.size; //地形的长宽高
Vector3 meshScale = new Vector3(size.x / (w - 1f) * vertexCountScale, 1, size.z / (h - 1f) * vertexCountScale);
w = (w - 1) / vertexCountScale + 1;
h = (h - 1) / vertexCountScale + 1;
Vector3[] vertices = new Vector3[w * h]; //最后生成w*h这么多个点
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
int index = j * w + i;
float z = terrainData.GetHeight(i * vertexCountScale, j * vertexCountScale); //采样高度图
vertices[index] = Vector3.Scale(new Vector3(i, z, j), meshScale); //缩放会原来的地形尺寸
}
}
同理我们uv怎么计算呢?
如果要搞清楚uv是怎么计算的,就需要明白贴图的size以及offset是什么意思?
可以看到这里的size,其实并不是我们通常意义的tile,它的意思就是size,就是定义了这个贴图的尺寸。那么地图被平铺多少个呢?使用地形的size.x/这里贴图的size的x即可。
而且每个贴图的wrapmode都是设置为repeat的方式:
Vector2 uvScale = new Vector2(1f / (w - 1f), 1f / (h - 1f)) * vertexCountScale *
(size.x /terrainData.splatPrototypes[0].tileSize.x);
new Vector2(1f / (w - 1f), 1f / (h - 1f))我们可以理解其实就是高度图的1/(w-1)以及1/(h-1)。
乘以vertexCountScale则是间隔多少的倍数。
而size.x/terrainData.splatPrototypes[0].tileSize.x,则是上面设置贴图里的size,也就是用地形的x除以这里的贴图的x,得到了水平平铺的倍数。
举例:
地形是88大小
高度图44大小
贴图是22大小
那么则要用贴图平铺44个贴图。
所以完整的代码是:
Vector3 meshScale = new Vector3(size.x / (w - 1f) * vertexCountScale, 1, size.z / (h - 1f) * vertexCountScale);
Vector2 uvScale = new Vector2(1f / (w - 1f), 1f / (h - 1f))
* vertexCountScale * (size.x / terrainData.splatPrototypes[0].tileSize.x); // [dev] 此处有问题,若每个图片大小不一,则出问题。日后改善
w = (w - 1) / vertexCountScale + 1;
h = (h - 1) / vertexCountScale + 1;
Vector3[] vertices = new Vector3[w * h];
Vector2[] uvs = new Vector2[w * h];
Vector4[] alphasWeight = new Vector4[w * h];
for (int i = 0; i < w; i++)
{
for (int j = 0; j < h; j++)
{
int index = j * w + i;
float z = terrainData.GetHeight(i * vertexCountScale, j * vertexCountScale);
Vector3 dddd = Vector3.Scale(new Vector3(i, z, j), meshScale);
vertices[index] = Vector3.Scale(new Vector3(i, z, j), meshScale);
uvs[index] = Vector2.Scale(new Vector2(i, j), uvScale);
// alpha map
int i2 = (int)(i * terrainData.alphamapWidth / (w - 1f));
int j2 = (int)(j * terrainData.alphamapHeight / (h - 1f));
i2 = Mathf.Min(terrainData.alphamapWidth - 1, i2);
j2 = Mathf.Min(terrainData.alphamapHeight - 1, j2);
var alpha0 = alphaMapData[j2, i2, 0];
var alpha1 = alphaMapData[j2, i2, 1];
//var alpha2 = alphaMapData[j2, i2, 2];
//var alpha3 = alphaMapData[j2, i2, 3];
alphasWeight[index] = new Vector4(alpha0, alpha1, 0, 0);
}
}
这里没有考虑到,每个贴图的size如果不一样的情况,所以为了简化纹理,可以将四个贴图的size设置为一样。
下面就是组织三角形网格了:
/*
* 三角形
* b c
* *******
* * * *
* * * *
* *******
* a d
*/
int[] triangles = new int[(w - 1) * (h - 1) * 6];
int triangleIndex = 0;
for (int i = 0; i < w - 1; i++)
{
for (int j = 0; j < h - 1; j++)
{
int a = j * w + i;
int b = (j + 1) * w + i;
int c = (j + 1) * w + i + 1;
int d = j * w + i + 1;
triangles[triangleIndex++] = a;
triangles[triangleIndex++] = b;
triangles[triangleIndex++] = c;
triangles[triangleIndex++] = a;
triangles[triangleIndex++] = c;
triangles[triangleIndex++] = d;
}
}
不解释了,很容易。
下面是用网格显示出来地形,唯一要注意的是,这里将四个贴图的alpha权重赋值给了mesh的tangent,实际上不是切线,只是作为后面的shader的中采样计算贴图混合方式的中间存储器。
同样我们还注意到,这里只有顶点信息、uv信息、三角形网格、alpha权重、但是没有法线信息,这个在这个博客中讲到:
https://blog.csdn.net/zr339361504/article/details/53352800
生成地形网格,我们需要保存顶点数据、瓦片贴图权重、瓦片uv坐标、光照贴图uv坐标、三角面、以及。由于与地形的光照部分直接使用烘培贴图,所以法线可以不用记录。
我们也可以计算出每个顶点的法线:
这个地方存疑点?到底是怎么计算法线,还需要研究下。
Mesh mesh = new Mesh();
mesh.vertices = vertices;
mesh.uv = uvs;
mesh.triangles = triangles;
mesh.tangents = alphasWeight; // 将地形纹理的比重写入到切线中
string transName = "[dev]MeshFromTerrainData";
var t = terrainObj.transform.parent.Find(transName);
if (t == null)
{
GameObject go = new GameObject(transName, typeof(MeshFilter), typeof(MeshRenderer), typeof(MeshCollider));
t = go.transform;
}
// 地形渲染
MeshRenderer mr = t.GetComponent<MeshRenderer>();
Material mat = mr.sharedMaterial;
if (!mat)
mat = new Material(Shader.Find("Custom/Environment/TerrainSimple"));
for (int i = 0; i < terrainData.splatPrototypes.Length; i++)
{
var sp = terrainData.splatPrototypes[i];
mat.SetTexture("_Texture" + i, sp.texture);
}
t.parent = terrainObj.transform.parent;
t.position = terrainObj.transform.position;
t.gameObject.layer = terrainObj.layer;
t.GetComponent<MeshFilter>().sharedMesh = mesh;
t.GetComponent<MeshCollider>().sharedMesh = mesh;
mr.sharedMaterial = mat;
t.gameObject.SetActive(true);
terrainObj.SetActive(false);
Debug.Log("Convert terrain to mesh finished!");
这样我们就根据terrain高度图,将terrain转换成mesh网格了。
下面就是写shader,用于采样贴图了:
Shader "Custom/Environment/TerrainSimple"
{
Properties
{
_Texture0 ("Texture 1", 2D) = "white" {}
_Texture1 ("Texture 2", 2D) = "white" {}
_Texture2 ("Texture 3", 2D) = "white" {}
_Texture3 ("Texture 4", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
LOD 200
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
sampler2D _Texture0;
sampler2D _Texture1;
sampler2D _Texture2;
sampler2D _Texture3;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float4 tangent : TANGENT;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float4 weight : TEXCOORD1;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.weight = v.tangent;
o.uv = v.uv;
return o;
}
fixed4 frag(v2f i) : SV_TARGET
{
fixed4 t0 = tex2D(_Texture0, i.uv);
fixed4 t1 = tex2D(_Texture1, i.uv);
fixed4 t2 = tex2D(_Texture2, i.uv);
fixed4 t3 = tex2D(_Texture3, i.uv);
fixed4 tex = t0 * i.weight.x + t1 * i.weight.y + t2 * i.weight.z + t3 * i.weight.w;
return tex;
}
ENDCG
}
}
Fallback "Diffuse"
}
Custom/Environment/TerrainSimple
这里是不支持光照计算的,也不支持光照贴图,所以完整的还需要参考:
http://www.cnblogs.com/jietian331/p/5831062.html
https://blog.csdn.net/zr339361504/article/details/53352800
但是http://www.cnblogs.com/jietian331/p/5831062.html 没有讲到法线的计算方式。
而https://blog.csdn.net/zr339361504/article/details/53352800讲到法线的计算方式,以及光照贴图的uv2计算。
两者的正确与否还是值得商榷的,但是这两篇也是唯一能够讲述怎么转terrain为mesh的博客了,如果有人知道其他的资料,麻烦留言给我,谢谢。
以上就是关于如果将unity自带的terrain转换为mesh的全部介绍了,下面我们将学习如果将mesh进行分割为小的mesh,并制定策略进行加载。