当前位置:首页 >教程首页 > 游戏设计 > 3D模型大师班 >分享数字建模方法——创造位面(2)

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

发布时间:2018-11-17 19:57:24


  创造有趣的位面

  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

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


学员作品赏析
  • 2101期学员李思庭作品

    2101期学员李思庭作品

  • 2104期学员林雪茹作品

    2104期学员林雪茹作品

  • 2107期学员赵凌作品

    2107期学员赵凌作品

  • 2107期学员赵燃作品

    2107期学员赵燃作品

  • 2106期学员徐正浩作品

    2106期学员徐正浩作品

  • 2106期学员弓莉作品

    2106期学员弓莉作品

  • 2105期学员白羽新作品

    2105期学员白羽新作品

  • 2107期学员王佳蕊作品

    2107期学员王佳蕊作品

专业问题咨询

你担心的问题,火星帮你解答
×

确定