一次简单的网格渲染

栏目: 后端 · 发布时间: 6年前

内容简介:在 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 。它有两个成员变量, meshsharedMeshmesh 就是这个类实例化自身独有的一个 Mesh ,而 sharedMesh 顾名思义就是共享的一个 Mesh 实例,如果对这个共享的实例修改,那么可能会影响到其它也共享使用这个 Mesh 实例的 MeshFilterMeshFilter 将网格传递给 MeshRender 渲染。

除了 MeshFilter ,渲染网格还需要一个类 MeshRender ,它主要是负责渲染相关的设置。包括很多属性:

  • materials 渲染所有的材质(可以多个)

  • receiveShadows 是否接受阴影

  • shadowCastingMode 阴影投射模式

  • ...

诸如上面还有很多其它属性,其中最重要的就是 materials ,如果不设置好渲染材质,那么 GPU 将不知道将要如何渲染你的网格。

接下来我们开始进行这次简单的网格渲染吧!

创建一个 GameObject,并给它添加上 MeshFilterMeshRenderMeshRender 需要设置好材质,并将我们最初创建的 Mesh 交给 MeshFilter :

GetComponent<MeshFilter>().mesh = _mesh;

接下来再回到我们的 Mesh ,这里具体说说 verticestrianglesuv

vertices

vertices 是顶点数据(通常我们所说模型的顶点们就是它)存放的地方,也称为顶点缓冲区。通常不同的多边形网格连接在一起可能会共享一些顶点数据,如果采用每个多边形单独存储其顶点数据的方式,那么顶点缓冲区就会包含冗余数据。因此通常都会有一个独立索引缓冲区( triangles )来索引每个多边形网格的顶点,从而使得顶点缓冲区更加紧凑(不存在重复顶点数据)。

这里的顶点缓冲区定义,也可以将 normalstangentsuv - 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 ),这里读取的第一组纹理坐标也就是在程序中赋予 Meshuv 。这里有个有意思的事情,如果只给 Meshuv 赋予了值,而 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 组 表示为当前 Meshuv4 设置上了自定义的第四组纹理坐标;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 设置的 TilingOffset

最后纹理采样是在片元着色器中进行的,使用变换过的纹理坐标对目标纹理进行采样并将颜色返回:

fixed4 color = tex2D(_MainTex, i.uv);
一次简单的网格渲染

到这里,从创建网格到渲染呈现基本完成。当然真实的过程复杂的多,比如复杂网格通常是由美术建模得到的,并将顶点、三角形网格、纹理坐标甚至法线等数据事先预制在模型中。GPU 渲染通常也是复杂的工程,Shader 编写可能涉及到顶点变换、光照计算、透明度混合等等许多事项。并且本文主要是对网格渲染做一个大致的讲解。

参考


以上所述就是小编给大家介绍的《一次简单的网格渲染》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Eloquent JavaScript

Eloquent JavaScript

Marijn Haverbeke / No Starch Press / 2011-2-3 / USD 29.95

Eloquent JavaScript is a guide to JavaScript that focuses on good programming techniques rather than offering a mish-mash of cut-and-paste effects. The author teaches you how to leverage JavaScript's......一起来看看 《Eloquent JavaScript》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具