内容简介:在 3D 计算机图形学中,可采用各种建模技术方案。如考察某一球体对象,可使用球体方程《计算机图形学-基于3D图形开发技术》对于建模人员来说使用图形软件即可创建基于多边形网格的 3D 对象,并且 GPU 对多边形网格进行了优化处理。因此在大多数时候,我们都是用多边形网格表现 3D 对象。
在 3D 计算机图形学中,可采用各种建模技术方案。如考察某一球体对象,可使用球体方程 \((x - Cx)^2 + (y - Cy)^2 + (z - Cz)^2 = r^2\) 这种基于隐式函数 \(f(x,y,z) = 0\) 的方式来表示隐式表面,也可根据拓扑实体(如顶点)采用显式方式表达球体对象,比如常用的多边形网格。
《计算机图形学-基于3D图形开发技术》
对于建模人员来说使用图形软件即可创建基于多边形网格的 3D 对象,并且 GPU 对多边形网格进行了优化处理。因此在大多数时候,我们都是用多边形网格表现 3D 对象。
下面我们就来看看网格是如何从构建成一个 3D 对象,到最终渲染成一副多彩的二维图像的。
从 Unity 中的 Mesh
说起
网格能表现 3D 对象,那么构成网格又需要哪些信息了? Unity 中, Mesh
类用于创建或者修改网格,它包含了许多的成员变量:
-
vertices
保存了顶点信息 -
triangles
所有的三角形顶点的索引(三个数据构成一个三角形) -
normals
顶点的法线信息 -
tangents
顶点的切线信息 -
uv - uv8
8 组纹理坐标 -
...
这里只列举了常用的一些需要的数据,还有更多的变量,读者可自行查阅 文档 。
在这里,我们先创建一个 Mesh
。
Mesh _mesh = new Mesh();
MeshFilter 和 MeshRender
在 Unity 中,创建的 Mesh
要能够表现在你面前需要 MeshFilter
类的帮助。 MeshFilter
很简单,主要就是一个中间层来让你在程序上动态使用 Mesh
。它有两个成员变量, mesh
和 sharedMesh
, mesh
就是这个类实例化自身独有的一个 Mesh
,而 sharedMesh
顾名思义就是共享的一个 Mesh
实例,如果对这个共享的实例修改,那么可能会影响到其它也共享使用这个 Mesh
实例的 MeshFilter
。 MeshFilter
将网格传递给 MeshRender
渲染。
除了 MeshFilter
,渲染网格还需要一个类 MeshRender
,它主要是负责渲染相关的设置。包括很多属性:
-
materials
渲染所有的材质(可以多个) -
receiveShadows
是否接受阴影 -
shadowCastingMode
阴影投射模式 -
...
诸如上面还有很多其它属性,其中最重要的就是 materials
,如果不设置好渲染材质,那么 GPU 将不知道将要如何渲染你的网格。
接下来我们开始进行这次简单的网格渲染吧!
创建一个 GameObject,并给它添加上 MeshFilter
和 MeshRender
, MeshRender
需要设置好材质,并将我们最初创建的 Mesh
交给 MeshFilter
:
GetComponent<MeshFilter>().mesh = _mesh;
接下来再回到我们的 Mesh
,这里具体说说 vertices
、 triangles
和 uv
。
vertices
vertices
是顶点数据(通常我们所说模型的顶点们就是它)存放的地方,也称为顶点缓冲区。通常不同的多边形网格连接在一起可能会共享一些顶点数据,如果采用每个多边形单独存储其顶点数据的方式,那么顶点缓冲区就会包含冗余数据。因此通常都会有一个独立索引缓冲区( triangles
)来索引每个多边形网格的顶点,从而使得顶点缓冲区更加紧凑(不存在重复顶点数据)。
这里的顶点缓冲区定义,也可以将 normals
、 tangents
和 uv - uv8
等包含进来,因为他们也是属于顶点的一部分,索引缓冲区同样对这些数据有效。
vertices
中的顶点数据决定了多边形网格可能会处的位置。接下来给 Mesh
的顶点缓冲区赋予一些顶点数据:
// mesh size int _xSize = 6, _ySize = 3; // mesh vertices size Vector3[] _vector3s = new Vector3[_xSize * _ySize]; for (int j = 0; j < _ySize; j++) { for (int i = 0; i < _xSize; i++) { _vector3s[j * _xSize + i] = new Vector3(i, j, 0); } } // assign to vertex buffer _mesh.vertices = _vector3s;
通过上面的代码,我们大致得到了空间中这样一些点:
\[ \left\{ \begin{matrix} (0,2,0) & (1,2,0) & (2,2,0) & (3,2,0) & (4,2,0) & (5,2,0) \\ (0,1,0) & (1,1,0) & (2,1,0) & (3,1,0) & (4,1,0) & (5,1,0) \\ (0,0,0) & (1,0,0) & (2,0,0) & (3,0,0) & (4,0,0) & (5,0,0) \end{matrix} \right\} \]
vertices
中存有上面这些顶点数据,依次从左到右,从下至上,索引如下:
\[ \left\{ \begin{matrix} 12 & 13 & 14 & 15 & 16 & 17 \\ 6 & 7 & 8 & 9 & 10 & 11 \\ 0 & 1 & 2 & 3 & 4 & 5 \end{matrix} \right\} \]
点可以连接起来组成多边形网格或线段。OpenGL 支持具有任意数量顶点的多边形,Direct3D 仅支持三角形网格,本文中也使用三角形网格。
triangles
triangles
(索引缓冲区)包含了三角形网格的顶点数据的索引(三个数据代表一个三角形)。三个索引组成一个三角形,顺序可能有顺时针(CW)或逆时针(CCW)两种情况。
首先我们尝试使用逆时针顺序(CCW)索引来组成一个三角形:
int[] triangles = new int[3]; triangles[0] = 0; triangles[1] = 1; triangles[2] = 6; // assign to indices buffer _mesh.triangles = triangles;
这种情况下运行看不到渲染出来的三角形面。为什么了?
在 Unity 中使用左手坐标系(LHS,X朝右Y朝上Z朝里),因此如果索引是逆时针构成三角形网格,那么根据左手定则三角面法线(法线方向指定正面)朝里(Z 轴正方向,背向摄像机),在 Shader 默认 Cull Back
情况下(背面将被剔除不渲染),摄像机拍摄的三角面(背面)将不会被渲染,因此看不到三角面。
在 Scene 编辑器中把场景转到 Z 轴正方向可以看到渲染出来的三角形网格(因为这一面才是正面)。解决上面问题有两个办法:
-
将索引修改为顺时针构成三角形网格,根据左手定则三角面法线将朝外(Z轴负方向),正面将朝向摄像机,因此在 Shader 默认
Cull Back
情况下就能看到渲染的三角形。 -
将 Shader
Cull Back
改成剔除正面Cull Front
或者不剔除Cull Off
即可。
我们使用顺时针索引顺序来作修改:
triangles[0] = 0; triangles[1] = 6; triangles[2] = 1; _mesh.triangles = triangles;
现在能看到渲染出的三角形,如下图:
把之前输入的顶点的全部按照顺时针顺序索引渲染三角形,用三角形网格组成一个平面,代码如下:
triangles = new int[_xSize * _ySize * 6]; for (int i = 0, m = 0; i < _ySize - 1; i++) { for (int j = 0; j < _xSize - 1; j++, m += 6) { triangles[m] = i * _xSize + j; triangles[m + 1] = triangles[m] + _xSize; triangles[m + 2] = i * _xSize + j + 1; triangles[m + 3] = triangles[m + 1]; triangles[m + 4] = triangles[m + 1] + 1; triangles[m + 5] = triangles[m + 2]; } } _mesh.triangles = triangles;
上面我们定义了 triangles
的大小为 _xSize * _ySize * 6
,为什么是这么多了。想象一下三角形组成的平面中,一个顶点最多可能被 6 个三角形共享,因此这个顶点会被索引 6 次,我们的代码中顶点的数量是 _xSize * _ySize
,因此索引缓冲区的大小就是 _xSize * _ySize * 6
。
uv
uv
(第一组纹理坐标)包含了模型顶点上面对应的纹理采样坐标。它的大小应该是同 vertices
一样大,从另一个角度来看,纹理坐标其实也是属于顶点数据的一部分,所以也可以看做是属于顶点缓冲区的。
当需要将一个 Texture 贴到我们的模型(网格组成)表面,就应当给模型每个顶点指定纹理采样坐标。纹理的 UV 值通常限定在了 [0,1] 之间,因此顶点的 uv
也应当属于 [0,1],不然纹理就可能采样错误(纹理不同的设置采样结果也不一样)。
下面为 Mesh
设置上第一组纹理坐标:
Vector2[] uvs = new Vector2[_vector3s.Length]; for (int i = 0; i < _ySize; i++) { for (int j = 0; j < _xSize; j++) { uvs[i * _xSize + j] = new Vector2((float)j / (_xSize - 1), (float)i / (_ySize - 1)); } } // assign to uv _mesh.uv = uvs;
纹理坐标设置了,最后就需要对理进行采样。为了弄清除纹理坐标是如何被应用到采样过程中的,我们通过 Unity 创建一个无光照 Shader 并将其用到渲染网格的材质上,看看其采样纹理部分代码:
// ... struct appdata { // ... float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; // ... }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; // ... o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { return tex2D(_MainTex, i.uv); } // ...
顶点着色器 vert
中,需要从顶点数据中读取顶点的模型坐标(语义 POSITION
)和第一组纹理坐标(语义 TEXCOORD0
),这里读取的第一组纹理坐标也就是在程序中赋予 Mesh
的 uv
。这里有个有意思的事情,如果只给 Mesh
的 uv
赋予了值,而 uv2 - uv8
没有赋值,那么在 Shader 中如果我读取第二组( TEXCOORD1
)或其它组纹理坐标( TEXCOORDx
)同样能得到正确的采样结果,说明 Unity 给 uv2 - uv8
赋予了 uv
的数据。
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
Mesh | uv | uv2 | uv3 | uv4 | uv5 | uv6 | uv7 | uv8 |
Set | x | x | x | 4 组 | x | x | 7 组 | x |
Shader | TEX0 | TEX1 | TEX2 | TEX3 | TEX4 | TEX5 | TEX6 | TEX7 |
Get | 0 | 0 | 0 | 4 组 | 4 组 | 4 组 | 7 组 | 7 组 |
表格中 Set 栏的 x
表示未给设置纹理坐标; 4 组
表示为当前 Mesh
的 uv4
设置上了自定义的第四组纹理坐标;Shader 栏中的 TEX0
是语义 TEXCOORD0
的缩写;Get 栏中的 0
表示 Shader 中获取的纹理坐标是 0, 4 组
表示 TEXCOORD4
读取的是程序中自定义的第四组纹理坐标
所以当设置某组 uv
时,若其后面的 uv(x)
没有设置,那么后边的将会被设置一份和 uv
一样的纹理坐标值,直至遇到某个 uv(x)
又被设置了值为止,后面没有设置纹理坐标的又会拥有刚刚设置的这份新的值。都不设置为默认值 0。
再回到 Shader 中,在顶点着色器中通过 Unity 内置宏 TRANSFORM_TEX
对顶点输入的纹理坐标进行的缩放和平移, TRANSFORM_TEX
实现大致如下:
o.uv = v.uv * _MainTex_ST.xy + _MainTex_ST.zw;
其中 _MainTex_ST
里面的四个值就对应了材质中对 Texture 设置的 Tiling
和 Offset
。
最后纹理采样是在片元着色器中进行的,使用变换过的纹理坐标对目标纹理进行采样并将颜色返回:
fixed4 color = tex2D(_MainTex, i.uv);
到这里,从创建网格到渲染呈现基本完成。当然真实的过程复杂的多,比如复杂网格通常是由美术建模得到的,并将顶点、三角形网格、纹理坐标甚至法线等数据事先预制在模型中。GPU 渲染通常也是复杂的工程,Shader 编写可能涉及到顶点变换、光照计算、透明度混合等等许多事项。并且本文主要是对网格渲染做一个大致的讲解。
参考
- 《计算机图形学-基于3D图形开发技术》
- http://catlikecoding.com/unity/tutorials/procedural-grid/
以上所述就是小编给大家介绍的《一次简单的网格渲染》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- [Grid 网格布局教程]显式网格和隐式网格之间的区别
- 精彩的无服务器时代:不仅有服务网格,还有事件网格
- 服务网格重蹈ESB的覆辙?为什么需要SMI服务网格接口? - samnewman
- 什么是服务网格?
- Kubernetes 服务网格工具对比
- [译] 初识 NGINX 服务网格
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。