游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

分享数字建模方法——创造位面(2)

发布时间:2013-09-12 17:18:28 Tags:,,,,

作者:Jayelinda Suridge

创造有趣的位面

Unity资产:Unity文件包包含了该教程的脚本和场景(请点击此处阅读本文第1部分)。

资源文件:只是面向那些没有Unity的资源文件。

我们已经学会了如何制作位面和盒子。这是最基本的建筑砖块。砖块可以基于各种有趣的方式结合并扩展。让我们着眼于一些例子。

房子

screen_house(from gamasutra)

screen_house(from gamasutra)

这只是一个带有附加位的盒子,但它以较小的规格出现在屏幕上时也是一个辨识度较高的形状。在填充遥远的背景时可以使用一个适当的网格。

让我们开始盒子的部分:

MeshBuilder meshBuilder = new MeshBuilder();

Vector3 upDir = Vector3.up * m_Height;
Vector3 rightDir = Vector3.right * m_Width;
Vector3 forwardDir = Vector3.forward * m_Length;

Vector3 farCorner = upDir + rightDir + forwardDir;
Vector3 nearCorner = Vector3.zero;

//shift pivot to centre-bottom:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

//Directional quad function (takes an offset and 2 directions)
BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);

这是源自最初教程的盒子代码,删去顶部和底部的四边形,包含了远近角落偏移从而让网格的原点位于中下位置。

screen_house_stage1(from gamasutra)

screen_house_stage1(from gamasutra)

下一步便是屋檐下的三角形。我们将自己编写一个BuildTriangle()函数:

void BuildTriangle(MeshBuilder meshBuilder, Vector3 corner0, Vector3 corner1, Vector3 corner2)
{
Vector3 normal = Vector3.Cross((corner1 – corner0), (corner2 – corner0)).normalized;

meshBuilder.Vertices.Add(corner0);
meshBuilder.UVs.Add(new Vector2(0.0f, 0.0f));
meshBuilder.Normals.Add(normal);

meshBuilder.Vertices.Add(corner1);
meshBuilder.UVs.Add(new Vector2(0.0f, 1.0f));
meshBuilder.Normals.Add(normal);

meshBuilder.Vertices.Add(corner2);
meshBuilder.UVs.Add(new Vector2(1.0f, 1.0f));
meshBuilder.Normals.Add(normal);

int baseIndex = meshBuilder.Vertices.Count – 3;

meshBuilder.AddTriangle(baseIndex, baseIndex + 1, baseIndex + 2);
}

我们在此创造半个四方形。这一函数选取了三个角落位置并直接将其插到顶点数组中。标准便是以同样的方法去创造我们的BuildQuad()函数:这是两个方向的向量积。我们需要估算这些方向而不是直接通过它们。

现在让我们将三角形置于前面和后面墙的上方:

MeshBuilder meshBuilder = new MeshBuilder();

Vector3 upDir = Vector3.up * m_Height;
Vector3 rightDir = Vector3.right * m_Width;
Vector3 forwardDir = Vector3.forward * m_Length;

Vector3 farCorner = upDir + rightDir + forwardDir;
Vector3 nearCorner = Vector3.zero;

//shift pivot to centre base:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);

//roof:

Vector3 roofPeak = Vector3.up * (m_Height + m_RoofHeight) + rightDir * 0.5f – pivotOffset;

Vector3 wallTopLeft = upDir – pivotOffset;
Vector3 wallTopRight = upDir + rightDir – pivotOffset;

BuildTriangle(meshBuilder, wallTopLeft, roofPeak, wallTopRight);
BuildTriangle(meshBuilder, wallTopLeft + forwardDir, wallTopRight + forwardDir,
roofPeak + forwardDir);

我们开始为前方的三角形估算三个角落。前面那堵墙的上方等于向上向量加上向右向量。三角形的最上方是屋顶的高度,刚好介于两个角落中间。

所有的这三个位置都需要pivotOffset从而能够与剩下的网格相对齐。

我们可以将这三个位置直接插入我们的Buildtriangle()功能去组成前方三角形。后方三角形需要做出两个改变。首先,所有的这三个位置都需要基于前方向量而发生偏移,从而能够将其置于房子的另一端。

其次,两个BuildTriangle() 参数需要进行交换。这能够在三角形内转变顶点的逆时针顺序,即意味着三角形的前后面现在是在不同方向上,因此第二个三角形所面对的是与第一个三角形不同的方向。

screen_house_stage1b(from gamasutra)

screen_house_stage1b(from gamasutra)

现在让我们来设置屋顶。这是三角形上的两个位面,与我们刚刚创造的三角形相对齐。但是我们希望房子带有屋檐,所以我们需要这这些位面稍微往外拉一点。

让我们估算所需要的一些值。m_RoofOverhangSide和m_RoofOverhangFront是预先定义好的变量—-屋檐的距离将分别延伸到房子的两侧。

我们将使用屋顶的最高点作为这些四边形的起点,所以我们需要估算从那里到每个墙角的方向。这将带给我们两个方向,即伴随着适当的斜坡与墙顶对齐:

Vector3 dirFromPeakLeft = wallTopLeft – roofPeak;
Vector3 dirFromPeakRight = wallTopRight – roofPeak;

然后我们将通过m_RoofOverhangSide所定义的数字去延伸这些方向向量:

dirFromPeakLeft += dirFromPeakLeft.normalized * m_RoofOverhangSide;
dirFromPeakRight += dirFromPeakRight.normalized * m_RoofOverhangSide;

接下来我们将把握roofPeak位置并将其移除房子的前方。我们也将延伸我们的向前向量,确保它足够长,从而能够覆盖房子的长度加上突出的部分:

roofPeak -= Vector3.forward * m_RoofOverhangFront;
forwardDir += Vector3.forward * m_RoofOverhangFront * 2.0f;Now we have all the information we need to build our roof quads:

BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakLeft);
BuildQuad(meshBuilder, roofPeak, dirFromPeakRight, forwardDir);

我们可以注意到方向与这两个函数调用是相接通的。这与之前在调用BuildTriangle()时转换参数的效果是一样的。这也会改变三角形的逆时针方向,让四边形面向不同的方向。现在两个屋顶四边形都是朝着房子外部的方向:

screen_house_stage2(from gamasutra)

screen_house_stage2(from gamasutra)

你将注意到一个问题。从这个角度看来,背向我们的屋顶四边形是看不到的。实际上,你唯一能够看到那面屋顶的情况是,从上往下看。这只适合于飞行游戏。

我们真正需要的是一个双面屋顶。也就是两个屋顶是位于同样的位置,但却朝着两个方向。幸运的是我们知道如何做到这点。基于我们之前创造的四边形使用该方法:

BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);

该代码与之前的一样,但是方向参数发生了改变。现在我们的屋顶四边形各朝着不同的方向了:

screen_house_stage2(from gamasutra)

screen_house_stage2(from gamasutra)

几乎要完成设置了,只差一步。着眼于最后图像中的线框。你将注意到我们可以通过屋顶看到墙的边缘。这是因为墙的那部分与屋顶的那部分位于同样的位置上,从而导致斑驳拥有那样的像素,特别是当深度缓存的精度较低或网格在较远的位置上时。修改方法很简单,我们只需要稍微提高整体屋顶的位置便可:

roofPeak += Vector3.up * m_RoofBias;

BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakLeft);
BuildQuad(meshBuilder, roofPeak, dirFromPeakRight, forwardDir);

BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);

现在就好多了:

screen_house_stage3(from gamasutra)

screen_house_stage3(from gamasutra)

让我们将所有的这些代码组合在一起:

MeshBuilder meshBuilder = new MeshBuilder();

Vector3 upDir = Vector3.up * m_Height;
Vector3 rightDir = Vector3.right * m_Width;
Vector3 forwardDir = Vector3.forward * m_Length;

Vector3 farCorner = upDir + rightDir + forwardDir;
Vector3 nearCorner = Vector3.zero;

//shift pivot to centre base:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

Vector3 undergroundOffset = Vector3.up * m_UndergroundDepth;
nearCorner -= undergroundOffset;
upDir += undergroundOffset;

BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);

//roof:

Vector3 roofPeak = Vector3.up * (m_Height + m_RoofHeight) + rightDir * 0.5f – pivotOffset;

Vector3 wallTopLeft = upDir – pivotOffset;
Vector3 wallTopRight = upDir + rightDir – pivotOffset;

BuildTriangle(meshBuilder, wallTopLeft, roofPeak, wallTopRight);
BuildTriangle(meshBuilder, wallTopLeft + forwardDir, wallTopRight + forwardDir,
roofPeak + forwardDir);

Vector3 dirFromPeakLeft = wallTopLeft – roofPeak;
Vector3 dirFromPeakRight = wallTopRight – roofPeak;

dirFromPeakLeft += dirFromPeakLeft.normalized * m_RoofOverhangSide;
dirFromPeakRight += dirFromPeakRight.normalized * m_RoofOverhangSide;

roofPeak -= Vector3.forward * m_RoofOverhangFront;
forwardDir += Vector3.forward * m_RoofOverhangFront * 2.0f;

//shift the roof slightly upward to stop it intersecting the top of the walls:
roofPeak += Vector3.up * m_RoofBias;

BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakLeft);
BuildQuad(meshBuilder, roofPeak, dirFromPeakRight, forwardDir);

BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);

栅栏

栅栏是很多环境中常会出现的东西。它可比想象好创造多了——毕竟它只是个盒子。

让我们开始设置。

void BuildPost(MeshBuilder meshBuilder, Vector3 position)
{
Vector3 upDir = Vector3.up * m_PostHeight;
Vector3 rightDir = Vector3.right * m_PostWidth;
Vector3 forwardDir = Vector3.forward * m_PostWidth;

Vector3 farCorner = upDir + rightDir + forwardDir + position;
Vector3 nearCorner = position;

//shift pivot to centre-bottom:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -rightDir, -forwardDir);
BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);
}

这与我们在之前教程中所写的盒子生成代码一样,减去最底断的四边形,并伴随着我们在房子中使用的同样的底部中心位置偏移。

现在我们正在使用一个位置偏移。我们的栅栏将拥有多个标杆,我们不希望它们都是彼此叠加着。

现在让我们创造一些标杆:

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

BuildPost(meshBuilder, offset);
}

一点都不复杂。这是调用了BuildPost() 和前所未有的偏移的循环。现在我们的栅栏如下:

screen_fence_stage1(from gamasutra)

screen_fence_stage1(from gamasutra)

现在设置横木:

void BuildCrossPiece(MeshBuilder meshBuilder, Vector3 start)
{
Vector3 upDir = Vector3.up * m_CrossPieceHeight;
Vector3 rightDir = Vector3.right * m_CrossPieceWidth;
Vector3 forwardDir = Vector3.forward * m_DistBetweenPosts;

Vector3 farCorner = upDir + rightDir + forwardDir + start;
Vector3 nearCorner = start;

BuildQuad(meshBuilder, nearCorner, forwardDir, rightDir);
BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -rightDir, -forwardDir);
BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);
}

这同样也与基本的盒子代码非常相似。需要注意的是我们使用了标杆间的距离作为每个木块的长度。通过这一方法我们可以确保木块能够碰触到下一个标杆。

回到创造标杆的循环中:

Vector3 prevCrossPosition = Vector3.zero;

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

BuildPost(meshBuilder, offset);

//crosspiece:

Vector3 crossPosition = offset;

//offset to the back of the post:
crossPosition += Vector3.back * m_PostWidth * 0.5f;

//offset the height:
crossPosition += Vector3.up * m_CrossPieceY;

if (i != 0)
BuildCrossPiece(meshBuilder, prevCrossPosition);

prevCrossPosition = crossPosition;
}

需要注意的是横木的起点位置便是标杆的位置,并基于标杆一半的宽度(游戏邦注:我们希望横木是在标杆之后,而不是伸到中间位置),以及一个预定义的高度值(否则横木将横卧在地上)发生偏移。

我们同样也从之前标杆生成的位置开始,并在现有的标杆上添加横木。这便意味着我们需要略过最初循环运行的网格生成内容。

让我们运行看看,并观看栅栏是否如下:

screen_fence_stage2(from gamasutra)

screen_fence_stage2(from gamasutra)

如果你想要的是较原始的效果,那这看起来还不错。但是让我们添加更多视觉趣味。我们想要看到较为粗糙且摇晃的栅栏。我们可以采取一些方法做到这点。

最简单的方法便是在标杆高度上添加一些随机变量。我们可以在BuildPost()函数中做到这点,即在向上向量中添加一个随机偏移。

float postHeight = m_PostHeight + Random.Range(-m_PostHeightVariation, m_PostHeightVariation);

Vector3 upDir = Vector3.up * postHeight);

现在我们的标杆如下,它们并未死板地钉在地上:

screen_fence_stage3(from gamasutra)

screen_fence_stage3(from gamasutra)

还有一件需要做的便是稍微倾斜横木,让它们不会完全基于同样高度钉在标杆上。

比起只是提供一个起点到BuildCrossPiece()函数中,我们想要提供一个起点和一个终点,并且都带有一个随机高度偏移:

Vector3 prevCrossPosition = Vector3.zero;

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

BuildPost(meshBuilder, offset);

//crosspiece:

Vector3 crossPosition = offset;

//offset to the back of the post:
crossPosition += Vector3.back * m_PostWidth * 0.5f;

float randomYStart = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);
float randomYEnd = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);

Vector3 crossYOffsetStart = Vector3.up * (m_CrossPieceY + randomYStart);
Vector3 crossYOffsetEnd = Vector3.up * (m_CrossPieceY + randomYEnd);

if (i != 0)
BuildCrossPiece(meshBuilder, prevCrossPosition + crossYOffsetStart,
crossPosition + crossYOffsetEnd);

prevCrossPosition = crossPosition;
}

现在我们的函数经历了2个位置,即从最初的标杆到现在的标杆,并且每个标杆都有其自身的高度偏移。

现在我们需要更新BuildCrossPiece()函数:

void BuildCrossPiece(MeshBuilder meshBuilder, Vector3 start, Vector3 end)
{
Vector3 dir = end – start;

Quaternion rotation = Quaternion.LookRotation(dir);

Vector3 upDir = rotation * Vector3.up * m_CrossPieceHeight;
Vector3 rightDir = rotation * Vector3.right * m_CrossPieceWidth;
Vector3 forwardDir = rotation * Vector3.forward * dir.magnitude;

Vector3 farCorner = upDir + rightDir + forwardDir + start;
Vector3 nearCorner = start;

BuildQuad(meshBuilder, nearCorner, forwardDir, rightDir);
BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -rightDir, -forwardDir);
BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);
}

我们在此所做的是基于起始位置估算旋转,并使用该旋转去倾斜横木。为了在整体的横木上使用旋转,我们只需要乘以所有的方向向量便可。

横木的长度是两个标杆间的距离。

screen_fence_stage4(from gamasutra)

screen_fence_stage4(from gamasutra)

现在看起来更顺眼了,但是我们的工作还未结束。让我们再次着眼于标杆。它们已经拥有高度变量,但是它们都是笔直朝上。我们将稍微倾斜每个标杆,就像它们偏移了地面一样。我们将在x和z轴生成随机旋转,并在BuildPost()中的方向向量中进行旋转而做到这点。

我们将在循环中生成选择,然后将其传达到BuildPosts函数中:

Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

float xAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
float zAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
Quaternion rotation = Quaternion.Euler(xAngle, 0.0f, zAngle);

BuildPost(meshBuilder, offset, rotation);

我们需要更新BuildPost()函数:

void BuildPost(MeshBuilder meshBuilder, Vector3 position, Quaternion rotation)
{
float postHeight = m_PostHeight +
Random.Range(-m_PostHeightVariation, m_PostHeightVariation);

Vector3 upDir = rotation * Vector3.up * postHeight;
Vector3 rightDir = rotation * Vector3.right * m_PostWidth;
Vector3 forwardDir = rotation * Vector3.forward * m_PostWidth;

在此我们能对所有方向向量使用旋转,即与我们在旋转横木时使用的方法一样。

现在为什么我们需要循环中的旋转值而不是在BuildPost()中估算?因为我们希望横木能够附着标杆,而不是静静地悬在空中。因为代码也需要知道旋转是什么。让我们重新设置循环:

Vector3 prevCrossPosition = Vector3.zero;
Quaternion prevRotation = Quaternion.identity;

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

float xAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
float zAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
Quaternion rotation = Quaternion.Euler(xAngle, 0.0f, zAngle);

BuildPost(meshBuilder, offset, rotation);

//crosspiece:

Vector3 crossPosition = offset;

//offset to the back of the post:
crossPosition += rotation * (Vector3.back * m_PostWidth * 0.5f);

float randomYStart = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);
float randomYEnd = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);

Vector3 crossYOffsetStart = prevRotation * Vector3.up * (m_CrossPieceY + randomYStart);
Vector3 crossYOffsetEnd = rotation * Vector3.up * (m_CrossPieceY + randomYEnd);

if (i != 0)
BuildCrossPiece(meshBuilder, prevCrossPosition + crossYOffsetStart,
crossPosition + crossYOffsetEnd);

prevCrossPosition = crossPosition;
prevRotation = rotation;
}

我们需要储存最初标杆的循环及其现在的偏移,如此我们的随机向上向量便能够进行适当的旋转。我们将同时旋转向上变量和标杆之后的偏移,从而在正确的位置上获得起始向量。

现在我们的栅栏如下:

screen_fence(from gamasutra)

screen_fence(from gamasutra)

一些额外的内容:每个标杆位置偏移

有时候,让栅栏沿着一个平坦的地面延伸还不够。我们也希望栅栏能够随着地面的起伏而变化。幸运的是所有的偏移都是基于标杆的位置。因为我们只需要改变这点就好,如此其余栅栏也将与之对齐。为了在标杆上使用高度偏移,我们只需要调整标杆位置的y值便可:

Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

offset.y += Mathf.Sin(offset.x) * 0.5f;

float xAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
float zAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
Quaternion rotation = Quaternion.Euler(xAngle, 0.0f, zAngle);

BuildPost(meshBuilder, offset, rotation);

简单来说,我使用了一个正弦波作为Y偏移,只是为了呈现出效果。而对于真正的栅栏,你便需要要获得当下位置地面的高度。我们可以使用Terrain.SampleHeight()做到这点,即从高度地图上抽样,参考生成地形的数据等等。这主要取决于你所面对的场景。

screen_fence_sinoffset(from gamasutra)

screen_fence_sinoffset(from gamasutra)

这便是第一部分内容,接下来我们会接续分析第二部分内容,即关于圆筒,球体和其它圆形物体。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦

Modelling by numbers: Part One B

by Jayelinda Suridge

Modelling by numbers

An introduction to procedural geometry

(This is the second part of a four part tutorial. If you haven’t already, you should check out Modelling by numbers: Part One A)

Part One B: Making planes interesting

Unity assets – Unity package file containing scripts and scenes for this tutorial (parts 1A and 1B).

Source files – Just the source files, for those that don’t have Unity.

OK, we’ve learned to make planes and boxes. Those are basic building blocks. Blocks that can be combined and extended in all kinds of interesting ways. Let’s look at a couple of examples…

A house

It’s just a box with some extra bits, but it’s a shape that’s both common and easily recognisable, even when very small on the screen. A good mesh to use when populating distant backgrounds.

Let’s start with the box part:

MeshBuilder meshBuilder = new MeshBuilder();

Vector3 upDir = Vector3.up * m_Height;
Vector3 rightDir = Vector3.right * m_Width;
Vector3 forwardDir = Vector3.forward * m_Length;

Vector3 farCorner = upDir + rightDir + forwardDir;
Vector3 nearCorner = Vector3.zero;

//shift pivot to centre-bottom:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

//Directional quad function (takes an offset and 2 directions)
BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);This is our box code from the previous tutorial, minus the top and bottom quads, and with the near and far corners offset so that the origin (pivot) of the mesh is at centre-bottom.

The next step is the triangles under the roof. We’re going to write ourselves a BuildTriangle() function:

void BuildTriangle(MeshBuilder meshBuilder, Vector3 corner0, Vector3 corner1, Vector3 corner2)
{
Vector3 normal = Vector3.Cross((corner1 – corner0), (corner2 – corner0)).normalized;

meshBuilder.Vertices.Add(corner0);
meshBuilder.UVs.Add(new Vector2(0.0f, 0.0f));
meshBuilder.Normals.Add(normal);

meshBuilder.Vertices.Add(corner1);
meshBuilder.UVs.Add(new Vector2(0.0f, 1.0f));
meshBuilder.Normals.Add(normal);

meshBuilder.Vertices.Add(corner2);
meshBuilder.UVs.Add(new Vector2(1.0f, 1.0f));
meshBuilder.Normals.Add(normal);

int baseIndex = meshBuilder.Vertices.Count – 3;

meshBuilder.AddTriangle(baseIndex, baseIndex + 1, baseIndex + 2);
}

We are building half a quad here. This function takes the three corner positions and plugs them straight into the vertex array. The normal is calculated the same way as in our BuildQuad() function: it’s the cross product of the two directional vectors. Only we need need to calculate those directions instead of having them passed in.

Now let’s put triangles at the top of our front and back walls:

MeshBuilder meshBuilder = new MeshBuilder();

Vector3 upDir = Vector3.up * m_Height;
Vector3 rightDir = Vector3.right * m_Width;
Vector3 forwardDir = Vector3.forward * m_Length;

Vector3 farCorner = upDir + rightDir + forwardDir;
Vector3 nearCorner = Vector3.zero;

//shift pivot to centre base:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);

//roof:

Vector3 roofPeak = Vector3.up * (m_Height + m_RoofHeight) + rightDir * 0.5f – pivotOffset;

Vector3 wallTopLeft = upDir – pivotOffset;
Vector3 wallTopRight = upDir + rightDir – pivotOffset;

BuildTriangle(meshBuilder, wallTopLeft, roofPeak, wallTopRight);
BuildTriangle(meshBuilder, wallTopLeft + forwardDir, wallTopRight + forwardDir,
roofPeak + forwardDir);

We start by calculating the three corners for the front triangle. The tops of the front wall are equal to the up vector, and the up plus the right vector. The top of the triangle is at the roof height, and is halfway between the two corners.

All three of these positions need to have pivotOffset subtracted from them to make them line up with the rest of the mesh.

We can plug these three positions straight into our Buildtriangle() function to form the front triangle. The back triangle needs two changes. First, all three positions need to be offset by the forward vector to put them at the other end of the house.

Secondly, two of the BuildTriangle() arguments need to be swapped. This reverses the winding order of the vertices within the triangle, meaning that the front and back faces of the triangle are now in opposite directions, and thus the second triangle faces in the opposite direction to the first.

Now for the roof. This is two planes on an angle, lining up with the triangles we just created. However, we want eaves on our house, so these planes need to be pulled out slightly.

Let’s calculate some of the values we’ll need. m_RoofOverhangSide and m_RoofOverhangFront are predefined variables – the distance the eaves are to extend out the sides and ends of the house, respectively.

We’re going to use the roof peak position as the starting point for these quads, so we need to calculate the direction from there to each of the wall corners. This will give us two directions with the correct slope to line up with the tops of the walls:

Vector3 dirFromPeakLeft = wallTopLeft – roofPeak;
Vector3 dirFromPeakRight = wallTopRight – roofPeak;Then we extend both these directional vectors by the amount defined by m_RoofOverhangSide:

dirFromPeakLeft += dirFromPeakLeft.normalized * m_RoofOverhangSide;
dirFromPeakRight += dirFromPeakRight.normalized * m_RoofOverhangSide;Next, we take our roofPeak position and shift it out in front of the house. We’ll also extend our forward vector, making it long enough to cover the length of the house plus an overhang at either end:

roofPeak -= Vector3.forward * m_RoofOverhangFront;
forwardDir += Vector3.forward * m_RoofOverhangFront * 2.0f;Now we have all the information we need to build our roof quads:

BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakLeft);
BuildQuad(meshBuilder, roofPeak, dirFromPeakRight, forwardDir);Notice that the directions are switched in these two function calls. This has the same effect as switching the arguments when calling BuildTriangle() before. It reverses the winding order of the triangles, making the quad face in the opposite direction. Now both roof quads are facing away from the sides of the house:

You’ll notice a problem with this. Viewed from this angle, the roof quad that’s facing away from us is invisible. In fact, the only time you’ll be able to see the roof properly is when looking from above. Good enough for a flying game, perhaps, but not for much else.

What we need is a double-sided quad. That is, two quads in the same position but facing the opposite way. Good thing we know how make a quad like that. Use this along with the quads we just built:

BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);The same code as before, but with the direction arguments switched. Our roof quads now each have an opposite side:

Almost done, just one little thing. Look closely at the wireframe in that last image. Notice how we can see the edge of the walls through the roof? This is due to that part of the wall and that part of the roof being in exactly the same place, which can lead to z-fighting at those pixels, particularly if the precision of the depth buffer is low and/or the mesh is far away (depth precision lessens as an object gets farther from a perspective camera). The fix is simple, we just lift the whole roof up very slightly:

roofPeak += Vector3.up * m_RoofBias;

BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakLeft);
BuildQuad(meshBuilder, roofPeak, dirFromPeakRight, forwardDir);

BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);There, much better:

As a recap, let’s put all that code together:

MeshBuilder meshBuilder = new MeshBuilder();

Vector3 upDir = Vector3.up * m_Height;
Vector3 rightDir = Vector3.right * m_Width;
Vector3 forwardDir = Vector3.forward * m_Length;

Vector3 farCorner = upDir + rightDir + forwardDir;
Vector3 nearCorner = Vector3.zero;

//shift pivot to centre base:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

Vector3 undergroundOffset = Vector3.up * m_UndergroundDepth;
nearCorner -= undergroundOffset;
upDir += undergroundOffset;

BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);

//roof:

Vector3 roofPeak = Vector3.up * (m_Height + m_RoofHeight) + rightDir * 0.5f – pivotOffset;

Vector3 wallTopLeft = upDir – pivotOffset;
Vector3 wallTopRight = upDir + rightDir – pivotOffset;

BuildTriangle(meshBuilder, wallTopLeft, roofPeak, wallTopRight);
BuildTriangle(meshBuilder, wallTopLeft + forwardDir, wallTopRight + forwardDir,
roofPeak + forwardDir);

Vector3 dirFromPeakLeft = wallTopLeft – roofPeak;
Vector3 dirFromPeakRight = wallTopRight – roofPeak;

dirFromPeakLeft += dirFromPeakLeft.normalized * m_RoofOverhangSide;
dirFromPeakRight += dirFromPeakRight.normalized * m_RoofOverhangSide;

roofPeak -= Vector3.forward * m_RoofOverhangFront;
forwardDir += Vector3.forward * m_RoofOverhangFront * 2.0f;

//shift the roof slightly upward to stop it intersecting the top of the walls:
roofPeak += Vector3.up * m_RoofBias;

BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakLeft);
BuildQuad(meshBuilder, roofPeak, dirFromPeakRight, forwardDir);

BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);A fence

Something common in many kinds of environments, a fence. It’s less complicated to make than it seems – it is just boxes, after all.

Let’s start with the posts.

void BuildPost(MeshBuilder meshBuilder, Vector3 position)
{
Vector3 upDir = Vector3.up * m_PostHeight;
Vector3 rightDir = Vector3.right * m_PostWidth;
Vector3 forwardDir = Vector3.forward * m_PostWidth;

Vector3 farCorner = upDir + rightDir + forwardDir + position;
Vector3 nearCorner = position;

//shift pivot to centre-bottom:
Vector3 pivotOffset = (rightDir + forwardDir) * 0.5f;
farCorner -= pivotOffset;
nearCorner -= pivotOffset;

BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -rightDir, -forwardDir);
BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);
}

This is basically the same as the box-generating code we wrote in the last tutorial, minus the bottom quad, and with the same centre-bottom position offset we used for our house.

Only now we’re providing a position offset. Our fence is going to have lots of posts, and we don’t want them all sitting on top of one another.

Right, let’s build some posts:

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

BuildPost(meshBuilder, offset);
}

Very uncomplicated. A loop that calls BuildPost() with an ever-increasing offset. Our fence is now looking something like this:

Now for the crosspieces.

void BuildCrossPiece(MeshBuilder meshBuilder, Vector3 start)
{
Vector3 upDir = Vector3.up * m_CrossPieceHeight;
Vector3 rightDir = Vector3.right * m_CrossPieceWidth;
Vector3 forwardDir = Vector3.forward * m_DistBetweenPosts;

Vector3 farCorner = upDir + rightDir + forwardDir + start;
Vector3 nearCorner = start;

BuildQuad(meshBuilder, nearCorner, forwardDir, rightDir);
BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -rightDir, -forwardDir);
BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);
}

Also very similar to the basic box code. Note that we use the distance between posts as the length of the piece. This way we can make sure that it always reaches the next post.

Going back to the loop that builds the posts:

Vector3 prevCrossPosition = Vector3.zero;

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

BuildPost(meshBuilder, offset);

//crosspiece:

Vector3 crossPosition = offset;

//offset to the back of the post:
crossPosition += Vector3.back * m_PostWidth * 0.5f;

//offset the height:
crossPosition += Vector3.up * m_CrossPieceY;

if (i != 0)
BuildCrossPiece(meshBuilder, prevCrossPosition);

prevCrossPosition = crossPosition;
}

Note that the starting position for the crosspiece is the post position, offset by half the width of the post (we want the crosspiece on the back of the post, not sticking through the middle), and by a predefined height value (otherwise the crosspiece would sit on the ground).

We’re also starting from the position generated by the previous post and having the crosspiece join that to the current one. This means that we need to skip the generation of the mesh the first time the loop runs.

Let’s run this and see how our fence is looking now:

Not bad, and fine if you want a pristine, well-made fence. But let’s add a little more visual interest. We want a slightly badly-made, wonky fence. There are a few things we can do to get this effect.

The simplest one is to add some random variation to the height of the posts. We can do this inside the BuildPost() function, by adding a random offset to the up directional vector:

float postHeight = m_PostHeight + Random.Range(-m_PostHeightVariation, m_PostHeightVariation);

Vector3 upDir = Vector3.up * postHeight);Now our posts look like they were hammered into the ground with much less precision:

Another thing to do is to tilt the crosspiece slightly, as if they aren’t nailed to the posts at exactly the same height all the way along.

Instead of providing just a start position to our BuildCrossPiece() function, we’re going to want to provide a start position and an end position, each with a random height offset:

Vector3 prevCrossPosition = Vector3.zero;

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

BuildPost(meshBuilder, offset);

//crosspiece:

Vector3 crossPosition = offset;

//offset to the back of the post:
crossPosition += Vector3.back * m_PostWidth * 0.5f;

float randomYStart = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);
float randomYEnd = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);

Vector3 crossYOffsetStart = Vector3.up * (m_CrossPieceY + randomYStart);
Vector3 crossYOffsetEnd = Vector3.up * (m_CrossPieceY + randomYEnd);

if (i != 0)
BuildCrossPiece(meshBuilder, prevCrossPosition + crossYOffsetStart,
crossPosition + crossYOffsetEnd);

prevCrossPosition = crossPosition;
}

Our function now passes two positions, from the previous post and the current one, each with its own height offset.

Now we need to update the BuildCrossPiece() function:

void BuildCrossPiece(MeshBuilder meshBuilder, Vector3 start, Vector3 end)
{
Vector3 dir = end – start;

Quaternion rotation = Quaternion.LookRotation(dir);

Vector3 upDir = rotation * Vector3.up * m_CrossPieceHeight;
Vector3 rightDir = rotation * Vector3.right * m_CrossPieceWidth;
Vector3 forwardDir = rotation * Vector3.forward * dir.magnitude;

Vector3 farCorner = upDir + rightDir + forwardDir + start;
Vector3 nearCorner = start;

BuildQuad(meshBuilder, nearCorner, forwardDir, rightDir);
BuildQuad(meshBuilder, nearCorner, rightDir, upDir);
BuildQuad(meshBuilder, nearCorner, upDir, forwardDir);

BuildQuad(meshBuilder, farCorner, -rightDir, -forwardDir);
BuildQuad(meshBuilder, farCorner, -upDir, -rightDir);
BuildQuad(meshBuilder, farCorner, -forwardDir, -upDir);
}

What we are doing here is calculating a rotation based on our start and end positions, and using that rotation to tilt the crosspiece. To apply the rotation to the entire crosspiece, we simply multiply all of our directional vectors.

The length of the crosspiece is the distance between the two points.

Looking better, but we’re not done yet. Let’s look at our posts again. They’ve got a height variation, but they’re all pointing straight upward. We’ll apply a slight lean to each one, as if it’s shifted in the ground. We’re going to do this to the posts by generating a random rotation in the x and z axis and then using that to rotate the directional vectors inside BuildPost().

We’ll generate the rotation inside the loop and then pass it to the BuildPosts function (the reason we need it in the loop will be seen at the next step):

Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

float xAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
float zAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
Quaternion rotation = Quaternion.Euler(xAngle, 0.0f, zAngle);

BuildPost(meshBuilder, offset, rotation);And we need to update our BuildPost() function:

void BuildPost(MeshBuilder meshBuilder, Vector3 position, Quaternion rotation)
{
float postHeight = m_PostHeight +
Random.Range(-m_PostHeightVariation, m_PostHeightVariation);

Vector3 upDir = rotation * Vector3.up * postHeight;
Vector3 rightDir = rotation * Vector3.right * m_PostWidth;
Vector3 forwardDir = rotation * Vector3.forward * m_PostWidth;

Here we apply the rotation to all of the directional vectors, just the way we did when rotating the crosspieces.

Now, why do we need the rotation value in the loop, rather than calculating it inside BuildPost()? It’s because we want the crosspieces to stay attached to the posts, instead of sitting still in mid-air as the posts shift away. Therefore that code also needs to know what the rotation is. Let’s revisit the loop:

Vector3 prevCrossPosition = Vector3.zero;
Quaternion prevRotation = Quaternion.identity;

for (int i = 0; i <= m_SectionCount; i++)
{
Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

float xAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
float zAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
Quaternion rotation = Quaternion.Euler(xAngle, 0.0f, zAngle);

BuildPost(meshBuilder, offset, rotation);

//crosspiece:

Vector3 crossPosition = offset;

//offset to the back of the post:
crossPosition += rotation * (Vector3.back * m_PostWidth * 0.5f);

float randomYStart = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);
float randomYEnd = Random.Range(-m_CrossPieceYVariation, m_CrossPieceYVariation);

Vector3 crossYOffsetStart = prevRotation * Vector3.up * (m_CrossPieceY + randomYStart);
Vector3 crossYOffsetEnd = rotation * Vector3.up * (m_CrossPieceY + randomYEnd);

if (i != 0)
BuildCrossPiece(meshBuilder, prevCrossPosition + crossYOffsetStart,
crossPosition + crossYOffsetEnd);

prevCrossPosition = crossPosition;
prevRotation = rotation;
}

We need to store the previous post’s rotation as well as its offset now, so that our randomised up vector can be rotated properly. We rotate both the up vectors and the back-of-post offset to get our start and end vectors in the correct place up against the post.

And there we have it:

Something extra: per-post position offsets
Sometimes, it’s not enough for the fence run along a completely flat terrain. We will want our fence to follow the height of the ground it sits on. The good news is that all of our offsets are based on the post position. Therefore, we only need to change this, and the rest of the fence will still line itself up properly. To apply a height offset to a post, simply adjust the y value of the post position:

Vector3 offset = Vector3.right * m_DistBetweenPosts * i;

offset.y += Mathf.Sin(offset.x) * 0.5f;

float xAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
float zAngle = Random.Range(-m_PostTiltAngle, m_PostTiltAngle);
Quaternion rotation = Quaternion.Euler(xAngle, 0.0f, zAngle);

BuildPost(meshBuilder, offset, rotation);

For simplicity, I’ve used a sine wave as the Y offset, just to show the effect. For an actual fence, you’ll want to get the height of your terrain at that position. This could be done using Terrain.SampleHeight() (if you’re using Unity terrain), with a raycast, by sampling a height map, by referencing the data that generates your terrain, or something else. It will depend on the setup of your scene.(source:gamasutra)


上一篇:

下一篇: