序
大概就是根据一个灰度图,生成一个地形。
分两步来实现吧;首先,用随机数生成地形;然后,根据灰度图生成地形。
小白,没啥基础,所以只能慢慢来。
首先,得有一些基本概念的:
00.一些基本概念
演示
我是个小白,所以,刚开始,来点直观的吧
新建了一个空物体gameobject,手动添加了3样东西给它:
- MeshFilter组件
- MeshRender组件
- 考虑到没有材质球会成粉色,所以新建了个默认的材质球给它。
前两个组件是主要的,下面的动图就简单的演示了这两个组件的作用。
顺便提一下,这里有个线框模式显示的开关。
大概有了个朦胧的认识:
- MeshFilter里的Mesh可以控制物体的形状
- MeshRender负责物体的显示
现在,开始文档里的正式介绍。
Mesh
Unity - Scripting API: Mesh (unity3d.com)
《inherits from Object》
里面按一定规则存着模型的数据,比如顶点什么的。【就是顶点着色器里的那个顶点】
看定义可能比较朦胧,看这个示例代码,就很清楚它是什么了:
MeshFilter
Unity - Manual: Mesh Filter component (unity3d.com)
这个组件,也很简单呐;里面就一个Mesh。。
具体可以这么用:
Unity - Scripting API: MeshFilter.mesh (unity3d.com)
从代码里可以看到,这个组件里的Mesh,就是上面的那个Mesh类
MeshRender
Unity - Manual: Mesh Renderer component (unity3d.com)
材质球,UnityShader,就是拖给这个组件的。再结合它的名字猜一下——它负责把网格画出来
小结
- MeshFilter和MeshRender是一对
- MeshFilter组件和Mesh类是一对
- 负责提供数据
- 如顶点在模型坐标系下的坐标
- MeshRender组件和材质球,shader是一对
- 定义了如何使用数据
- 比如在片元着色器里把quad给discard成ball,point sprite就是这么来的
后面就是按着视频来了。
01.大小为1的平面
这个是视频里的代码。
结合上面介绍的基本概念,大概知道它在干什么吧。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Terrian : MonoBehaviour
{
public float width = 0.1f;
MeshRenderer meshRenderer;
MeshFilter meshFilter;
// 用来存放顶点数据
List<Vector3> verts;
List<int> indices;
private void Awake()
{
}
private void Start()
{
verts = new List<Vector3>();
indices = new List<int>();
meshRenderer = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
Generate();
}
public void Generate()
{
ClearMeshData();
// 把数据填写好
AddMeshData();
// 把数据传递给Mesh,生成真正的网格
Mesh mesh = new Mesh();
mesh.vertices = verts.ToArray();
//mesh.uv = uvs.ToArray();
mesh.triangles = indices.ToArray();
mesh.RecalculateNormals();
mesh.RecalculateBounds();
meshFilter.mesh = mesh;
}
void ClearMeshData()
{
verts.Clear();
indices.Clear();
}
void AddMeshData()
{
verts.Add(new Vector3(0, 0, 0));
verts.Add(new Vector3(0, 0, 1));
verts.Add(new Vector3(1, 0, 1));
verts.Add(new Vector3(1, 0, 0));
indices.Add(0); indices.Add(1); indices.Add(2);
indices.Add(0); indices.Add(2); indices.Add(3);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Terrian : MonoBehaviour
{
public float width = 0.1f;
MeshRenderer meshRenderer;
MeshFilter meshFilter;
// 用来存放顶点数据
List<Vector3> verts;
List<int> indices;
private void Awake()
{
}
private void Start()
{
verts = new List<Vector3>();
indices = new List<int>();
meshRenderer = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
Generate();
}
public void Generate()
{
ClearMeshData();
// 把数据填写好
AddMeshData();
// 把数据传递给Mesh,生成真正的网格
Mesh mesh = new Mesh();
mesh.vertices = verts.ToArray();
//mesh.uv = uvs.ToArray();
mesh.triangles = indices.ToArray();
mesh.RecalculateNormals();
mesh.RecalculateBounds();
meshFilter.mesh = mesh;
}
void ClearMeshData()
{
verts.Clear();
indices.Clear();
}
void AddMeshData()
{
verts.Add(new Vector3(0, 0, 0));
verts.Add(new Vector3(0, 0, 1));
verts.Add(new Vector3(1, 0, 1));
verts.Add(new Vector3(1, 0, 0));
indices.Add(0); indices.Add(1); indices.Add(2);
indices.Add(0); indices.Add(2); indices.Add(3);
}
}
在上面的那个演示的基础上,把它拖给空物体,就可以了。
02.更大规模的平面
灌数据到Mesh的原理
主要就俩数组,一个是顶点,一个是索引。
视频里这个图挺好的。
顶点数据
索引数据
稍微复杂一点,因为顶点是单独的,这个是相互关联的。
很形象的图,涉及到二维逻辑地址和一维物理地址的换算。
从特殊到一般:
最终应用【顺序是比较重要的,因为单面剔除,cull on,cull off之类的】
试一试
修改前:
void AddMeshData()
{
verts.Add(new Vector3(0, 0, 0));
verts.Add(new Vector3(0, 0, 1));
verts.Add(new Vector3(1, 0, 1));
verts.Add(new Vector3(1, 0, 0));
indices.Add(0); indices.Add(1); indices.Add(2);
indices.Add(0); indices.Add(2); indices.Add(3);
}
void AddMeshData()
{
verts.Add(new Vector3(0, 0, 0));
verts.Add(new Vector3(0, 0, 1));
verts.Add(new Vector3(1, 0, 1));
verts.Add(new Vector3(1, 0, 0));
indices.Add(0); indices.Add(1); indices.Add(2);
indices.Add(0); indices.Add(2); indices.Add(3);
}
修改后
void AddMeshData()
{
int N = 10;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以最外层的循环是z不是x
{
for(int x = 0; x < N; ++x)
{
Vector3 temp = new Vector3(x, 0, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
void AddMeshData()
{
int N = 10;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以最外层的循环是z不是x
{
for(int x = 0; x < N; ++x)
{
Vector3 temp = new Vector3(x, 0, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
结果,符合预期;在原点那里放了个cube,作参照。
03.从平面到地形
这个不难,加一行
void AddMeshData()
{
int N = 10;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
float height = Random.Range(0.1f, 1.0f);//随机加个高度
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
void AddMeshData()
{
int N = 10;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
float height = Random.Range(0.1f, 1.0f);//随机加个高度
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
结果,有高度起伏了。
这个是10*10规模的,更大的规模也是一样的。计算机擅长重复。
04.从随机数到灰度图
准备
首先,得有个地形灰度图;这里用的是这个:
其次,得能从C#脚本里读到纹理的值。
Unity - Scripting API: Texture2D (unity3d.com)
GetPixel函数
Unity - Scripting API: Texture2D.GetPixel (unity3d.com)
解释的很详细了:
从下图可以看出,这个xy不是归一化后的uv:
这个返回值color,倒是归一化的:Unity - Scripting API: Color (unity3d.com)
视频里用的是更快的这个函数:
Unity - Scripting API: Texture2D.GetPixels32 (unity3d.com)
但是,我是小白,能搞出来就已经是极限了,哪里还顾得上什么性能问题。
试一试
代码
接着改那个函数就行
void AddMeshData()
{
int N = 100;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);//没归一化的uv
int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
float height = grayValue*heightRatio;//灰度值范围是[0,1],所以得缩放一下
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
……//这部分是没改,省略
}
}
}
void AddMeshData()
{
int N = 100;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);//没归一化的uv
int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
float height = grayValue*heightRatio;//灰度值范围是[0,1],所以得缩放一下
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
……//这部分是没改,省略
}
}
}
符合预期吧
完整的代码
新建一个空物体,上面添加上MeshFilter,MeshRender两个组件,然后把这个脚本拖给这个空物体,再点击play,大概就行了。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Terrian : MonoBehaviour
{
public Texture2D texture2dHeightMap;
[Range(1,100)]
public float heightRatio = 30.0f;//一个系数,控制地形总体的高度的
MeshRenderer meshRenderer;
MeshFilter meshFilter;
// 用来存放顶点数据
List<Vector3> verts;
List<int> indices;
private void Awake()
{
}
private void Start()
{
verts = new List<Vector3>();
indices = new List<int>();
meshRenderer = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
}
private void Update()
{
Generate();
}
public void Generate()
{
ClearMeshData();
// 把数据填写好
AddMeshData();
// 把数据传递给Mesh,生成真正的网格
Mesh mesh = new Mesh();
mesh.vertices = verts.ToArray();
mesh.triangles = indices.ToArray();
mesh.RecalculateNormals();
mesh.RecalculateBounds();
meshFilter.mesh = mesh;
}
void ClearMeshData()
{
verts.Clear();
indices.Clear();
}
void AddMeshData()
{
int N = 100;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);
int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
float height = grayValue*heightRatio;
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Terrian : MonoBehaviour
{
public Texture2D texture2dHeightMap;
[Range(1,100)]
public float heightRatio = 30.0f;//一个系数,控制地形总体的高度的
MeshRenderer meshRenderer;
MeshFilter meshFilter;
// 用来存放顶点数据
List<Vector3> verts;
List<int> indices;
private void Awake()
{
}
private void Start()
{
verts = new List<Vector3>();
indices = new List<int>();
meshRenderer = GetComponent<MeshRenderer>();
meshFilter = GetComponent<MeshFilter>();
}
private void Update()
{
Generate();
}
public void Generate()
{
ClearMeshData();
// 把数据填写好
AddMeshData();
// 把数据传递给Mesh,生成真正的网格
Mesh mesh = new Mesh();
mesh.vertices = verts.ToArray();
mesh.triangles = indices.ToArray();
mesh.RecalculateNormals();
mesh.RecalculateBounds();
meshFilter.mesh = mesh;
}
void ClearMeshData()
{
verts.Clear();
indices.Clear();
}
void AddMeshData()
{
int N = 100;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);
int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
float height = grayValue*heightRatio;
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
}
bug的修复
上面的代码,存在内存泄漏的问题。
尝试避免内存泄漏:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Terrain : MonoBehaviour
{
public Texture2D texture2dHeightMap;
[Range(1,100)]
public float heightRatio = 30.0f;//一个系数,控制地形总体的高度的
MeshFilter meshFilter;
Mesh mesh; // 之前是先获取meshfilter,再获取mesh,改成直接一步到位了.
// 用来存放顶点数据
List<Vector3> verts;
List<int> indices;
private void Awake()
{
}
private void Start()
{
verts = new List<Vector3>();
indices = new List<int>();
meshFilter = GetComponent<MeshFilter>();
mesh = GetComponent<MeshFilter>().mesh;
}
private void Update()
{
Generate();
}
public void Generate()
{
ClearMeshData();
// 把数据填写好
AddMeshData();
// 把数据传递给Mesh,生成真正的网格
// Mesh mesh = new Mesh(); // 每帧都在new,造成了内存泄漏.把这行绕过去,就能解决内存泄漏问题.
mesh.vertices = verts.ToArray();
mesh.triangles = indices.ToArray();
mesh.RecalculateNormals();
mesh.RecalculateBounds();
// meshFilter.mesh = mesh;
}
void ClearMeshData()
{
verts.Clear();
indices.Clear();
}
void AddMeshData()
{
int N = 100;
//01填充顶点数据
for (int z = 0; z < N; ++z)//按先x后z的顶点排列顺序,所以先循环的是z
{
for(int x = 0; x < N; ++x)
{
int u = Mathf.FloorToInt(1.0f * x / N * texture2dHeightMap.width);
int v = Mathf.FloorToInt(1.0f * z / N * texture2dHeightMap.height);
float grayValue = texture2dHeightMap.GetPixel(u,v).grayscale;
float height = grayValue*heightRatio;
Vector3 temp = new Vector3(x, height, z);
verts.Add(temp);
}
}
//02填充索引数据
for(int z = 0; z < N - 1; ++z)
{
for(int x = 0; x < N - 1; ++x)
{
int index_lb = z * N + x;//index of the left bottom vertex. lb = left bottom
int index_lt = (z + 1) * N + x;
int index_rt = (z + 1) * N + x + 1;
int index_rb = z * N + x + 1;
indices.Add(index_lb);indices.Add(index_lt);indices.Add(index_rt);
indices.Add(index_rt);indices.Add(index_rb);indices.Add(index_lb);
}
}
}
}
结果: