
Unity3D C#数学系列之创建圆柱体
我们知道3D Max中可以很方便的创建出一个圆柱体,那么在Unity中能否用代码创建出一个圆柱体呢,当然可以的,在Unity里面想怎么玩都行的。我们今天来看看在Unity中用代码创建一个圆柱体。
废话少说,先看效果。
1 逻辑梳理
用代码创建圆柱体逻辑其实很简单,关键点就两个:
1.生成圆柱体的网格
2.将网格用合适的Shader渲染出来
1.1 生成圆柱体网格
unity中要渲染一个物体,需要先添加两个组件,一是MeshFilter,二是MeshRenderer。
MeshFilter 网格过滤器
其唯一的作用就是用来指定Mesh。
它有两个属性mesh,sharedMesh;可以把mesh理解为值传递,修改网格只会影响到这一个物体;而shardeMesh是引用传递,修改网格会应用到所有使用这个网格的物体,并且会持久化。
MeshRenderer 网格渲染器
MeshRender 负责渲染 MeshFilter 指定的 Mesh,在 Transform 的位置渲染这个Mesh。
MeshRender参数主要指定是否产生和接受阴影,以及使用的Meterial。
当然光有这两个组件还不行,我们还得生成网格,并将网格幅值给MeshFilter组件。
unity中创建一个网格的步骤如下(这里直接以圆柱体网格为例了):
①先new一个Mesh
Mesh mesh = new UnityEngine.Mesh();
有个问题,我们能不能只使用一个Mesh,然后用不同的材质去渲染Mesh的不同部位呢,就像上面Demo中的网格线和圆柱体表面?当然可以的。
一个Mesh只有一份顶点,但是可以有多个submesh,通过Mesh的subMeshCount属性指定。每个submesh有自己的material,各个submesh的材质通过Mesh的materials(材质数组来确定)。代码类似如下。
mesh.subMeshCount = 2; // 这里指定2个submesh
meshRenderer.materials = new Material[]{
mat1, // 材质球1
mat2, // 材质球2
};
②计算出Mesh的所有顶点坐标,并指定给Mesh
Mesh的顶点坐标就是模型坐标,是相对于物体的位置的。
Demo中圆柱体的顶点只计算了下底面和上底面圆周的顶点,上底面、下底面各20个顶点。
List<Vector3> vertices = new List<Vector3>();
// 计算圆柱体上下底面顶点(上下各20个)
const int cnt = 20;
float deltaRad = Mathf.PI * 2 / cnt;
for (int i = 0; i < cnt; i++)
{
float rad = i * deltaRad;
float x = radius * Mathf.Sin(rad);
float z = radius * Mathf.Cos(rad);
vertices.Add(new Vector3(x, 0, z)); // 下底面顶点
vertices.Add(new Vector3(x, height, z)); // 上底面顶点
}
// 将顶点指定给网格
mesh.SetVertices(vertices);
③指定Mesh的顶点索引数组,及MeshTopology(网格的拓扑结构,不知道这样翻译对不对)
计算出网格所有顶点后,这些顶点该如何使用?是组成三角面还是组成连接成线?如果组合成三角面,那么哪几个顶点组合呢?这就需要我们设定顶点索引数组和MeshTopology。顶点索引数组是和MeshTopology对应的,MeshTopology变了,顶点索引数组也要跟着调整。
Unity中MeshTopology有五种。
public enum MeshTopology
{
// 三角面
Triangles = 0,
// 四边形
Quads = 2,
// 线段
Lines = 3,
// 线带
LineStrip = 4,
// 点
Points = 5,
}
比如说我们打算将MeshTopology设置为三角面,那么我们的顶点索引数组长度应该就是3的倍数(因为一个面要3个顶点嘛)。顶点索引数组中,第0、1、2组成第一个三角面,第3、4、5组成第二个三角面,以此类推。
但是要注意顶点的排列顺序,默认情况下,Unity中的所有Shader都是单面的,它都把反面的渲染给关闭掉了(Cull On)。所以顶点索引顺序不对,Shader中又没有Cull Off的话,我们可能就看不见那个面(因为是反面,被剔除啦)。
那怎样的顶点索引顺序才是正面呢?
很简单,三角面的法线方向和我们的视线方向相反,那么我们看到的这个面就是正面,反之为反面。
那么如何判断三角面的法线方向呢?
也很简单,假设我们的顶点索引排列顺序是ABC。
由于Unity局部坐标使用的是左手坐标系,那么举起你的左手,大拇指竖着,食指和小指并拢,由A朝向B弯曲,大拇指的方向就是这个面的法线方向,所以下图中ABC的的法线是朝屏幕外的。而我们是看向屏幕的,即我们的视线方向与面的法线方向相反,所以ABC这个面为正面。
核心方法就是Mesh的SetIndices。
// 最后的0是subMesh的索引值,从0开始
mesh.SetIndices(indexList.ToArray(), MeshTopology.Triangles, 0);
Demo中绘制侧面的方法。
// 侧面
List<int> indexList = new List<int>();
for (int i = 0; i < cnt; i++)
{
if (i == cnt - 1)
{
// 最后一个点和第一个点连接起来
indexList.Add(2 * i);
indexList.Add(2 * i + 1);
indexList.Add(0);
indexList.Add(0);
indexList.Add(2 * i + 1);
indexList.Add(1);
}
else
{
// 要注意顶点的排列顺序,如果shader没有Cull Off,这个面可能不可见
indexList.Add(2 * i);
indexList.Add(2 * i + 1);
indexList.Add(2 * (i + 1));
indexList.Add(2 * (i + 1));
indexList.Add(2 * i + 1);
indexList.Add(2 * i + 3);
}
}
mesh.SetIndices(indexList.ToArray(), MeshTopology.Triangles, 0);
MeshTopology中的Lines和LineStrap都可以用来画线,但是顶点索引数组有点不同。
假设顶点索引数组为[0, 1, 2, 3, 4,5]。
那么MeshTopology为Lines时,0-1、2-3、4-5将分别组成一条线段,共3条线段。
而MeshTopology为LineStrap时,0-1、1-2、2-3、3-4、4-5将分别组成一条线段,共5条线段。
Demo中使用Lines模式来画网格线。
// 网格线
List<int> gridIndexList = new List<int>();
for (int i = 0; i < cnt; i++)
{
if (i == cnt - 1)
{
gridIndexList.Add(2 * i);
gridIndexList.Add(0);
gridIndexList.Add(2 * i + 1);
gridIndexList.Add(1);
gridIndexList.Add(2 * i);
gridIndexList.Add(2 * i + 1);
gridIndexList.Add(2 * i + 1);
gridIndexList.Add(0);
}
else
{
gridIndexList.Add(2 * i);
gridIndexList.Add(2 * (i + 1));
gridIndexList.Add(2 * i + 1);
gridIndexList.Add(2 * i + 3);
gridIndexList.Add(2 * i);
gridIndexList.Add(2 * i + 1);
gridIndexList.Add(2 * i + 1);
gridIndexList.Add(2 * (i + 1));
}
}
// 这里subMesh为1了
mesh.SetIndices(gridIndexList.ToArray(), MeshTopology.Lines, 1);
④指定Mesh的法线和uv坐标
如果网格需要贴上贴图和光照则需要设置Mesh的法线和uv坐标,每个顶点都需要设置。
// 法线
mesh.normals
// 贴图
mesh.uv
法线也可通过mesh.RecalculateNormals()
自动计算出来,但MeshTopology为Lines或Points会报错。
由于Demo中不需要给圆柱体计算光照和贴上贴图,所以Demo中没设置它们。
1.2 一个简单的Shader
由于Shader是透明的,所以关闭深度写入ZWrite Off
。由于要两面都要可见,所以关闭剔除Cull Off
。
Shader "Custom/Cylinder"
{
Properties
{
_MainColor ("Main Color", Color) = (1, 1, 1, 0.5)
}
SubShader
{
Tags { "Queue" = "Transparent" "RenderType" = "Transparent" }
LOD 100
Pass
{
ZWrite Off
Cull Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
fixed4 _MainColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _MainColor;
}
ENDCG
}
}
}
1.3 相机操控
Demo中我们可以360拖动、放大缩小、旋转查看物体。
之前已经实现过了,具体见这篇文章《Unity3D相机操控(完整模拟Scene视图操作)》。
2 源码
项目放到这儿了。
链接:https://pan.baidu.com/s/1DgHNuG-nBPSrE5bh4f03kA
提取码:6yg8