创造有趣的位面 Unity资产:Unity文件包包含了该教程的脚本和场景(
阅读第1部分)。
资源文件:只是面向那些没有Unity的资源文件。
我们已经学会了如何制作位面和盒子。这是最基本的建筑砖块。砖块可以基于各种有趣的方式结合并扩展。让我们着眼于一些例子。
房子screen_house
这只是一个带有附加位的盒子,但它以较小的规格出现在屏幕上时也是一个辨识度较高的形状。在填充遥远的背景时可以使用一个适当的网格。
让我们开始盒子的部分:
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
下一步便是屋檐下的三角形。我们将自己编写一个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
现在让我们来设置屋顶。这是三角形上的两个位面,与我们刚刚创造的三角形相对齐。但是我们希望房子带有屋檐,所以我们需要这这些位面稍微往外拉一点。
让我们估算所需要的一些值。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
你将注意到一个问题。从这个角度看来,背向我们的屋顶四边形是看不到的。实际上,你唯一能够看到那面屋顶的情况是,从上往下看。这只适合于飞行游戏。
我们真正需要的是一个双面屋顶。也就是两个屋顶是位于同样的位置,但却朝着两个方向。幸运的是我们知道如何做到这点。基于我们之前创造的四边形使用该方法:
BuildQuad(meshBuilder, roofPeak, dirFromPeakLeft, forwardDir);
BuildQuad(meshBuilder, roofPeak, forwardDir, dirFromPeakRight);
该代码与之前的一样,但是方向参数发生了改变。现在我们的屋顶四边形各朝着不同的方向了:
screen_house_stage2
几乎要完成设置了,只差一步。着眼于最后图像中的线框。你将注意到我们可以通过屋顶看到墙的边缘。这是因为墙的那部分与屋顶的那部分位于同样的位置上,从而导致斑驳拥有那样的像素,特别是当深度缓存的精度较低或网格在较远的位置上时。修改方法很简单,我们只需要稍微提高整体屋顶的位置便可:
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
让我们将所有的这些代码组合在一起:
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
现在设置横木:
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
如果你想要的是较原始的效果,那这看起来还不错。但是让我们添加更多视觉趣味。我们想要看到较为粗糙且摇晃的栅栏。我们可以采取一些方法做到这点。
最简单的方法便是在标杆高度上添加一些随机变量。我们可以在BuildPost()函数中做到这点,即在向上向量中添加一个随机偏移。
float postHeight = m_PostHeight + Random.Range(-m_PostHeightVariation, m_PostHeightVariation);
Vector3 upDir = Vector3.up * postHeight);
现在我们的标杆如下,它们并未死板地钉在地上:
screen_fence_stage3
还有一件需要做的便是稍微倾斜横木,让它们不会完全基于同样高度钉在标杆上。
比起只是提供一个起点到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
现在看起来更顺眼了,但是我们的工作还未结束。让我们再次着眼于标杆。它们已经拥有高度变量,但是它们都是笔直朝上。我们将稍微倾斜每个标杆,就像它们偏移了地面一样。我们将在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
一些额外的内容:每个标杆位置偏移
有时候,让栅栏沿着一个平坦的地面延伸还不够。我们也希望栅栏能够随着地面的起伏而变化。幸运的是所有的偏移都是基于标杆的位置。因为我们只需要改变这点就好,如此其余栅栏也将与之对齐。为了在标杆上使用高度偏移,我们只需要调整标杆位置的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
这便是第一部分内容,接下来我们会接续分析第二部分内容,即关于圆筒,球体和其它圆形物体。