netlib.narod.ru< Назад | Оглавление | Далее >

Класс Terrain

В этом разделе вы создадите класс для работы с ландшафтом с именем Terrain, где сперва создадите методы для загрузки карты высот, генерации трехмерной сетки и рисования ландшафта. В дальнейшем вы добавите в этот класс новые методы, используемые для запроса высоты ландшафта в заданной точке и проверки столкновений.

Загрузка карты высот ландшафта

Первый этап генерации ландшафта — чтение его данных из карты высот. Поскольку карта высот хранится как файл RAW, вы можете использовать FileStream для чтения ее данных и хранить их в массиве byte[]. Заметьте, что поскольку у карты высот нет заголовка, вам необходимо знать ее размер, и он должен соответствовать размеру решетки вершин. Для чтения и сохранения данных карты высот используйте следующий код:

// Открытие файла карты высот
FileStream fileStream = File.OpenRead(heightmapFileName);

int heightmapSize = vertexCountX * vertexCountZ;

// Чтение данных карты высот
heightmap = new byte[heightmapSize];
fileStream.Read(heightmap, 0, heightmapSize);
fileStream.Close();

В показанном коде вы читаете и сохраняете карту высот того же самого размера, что и решетка вершин, которую вы собираетесь создать. Вы определяете размер решетки вершин через переменные vertexCountX и vertexCountZ, являющиеся параметрами, используемыми для загрузки карты высот. Переменная vertexCountX определяет количество вершин в строке сетки (по оси X), а vertexCountZ определяет количество вершин в столбце сетки (по оси Z).

Вы храните данные карты высот в переменной heightmap, являющейся атрибутом класса Terrain. Заметьте, что данные карты высот вам потребуются позже, чтобы вы могли запросить высоту определенной точки ландшафта. После чтения карты высот вы можете сгенерировать сетку ландшафта, используя эти данные. Вы создадите метод GenerateTerrainMesh, чтобы генерировать сетку ландшафта, составленную из индексов и вершин. Метод GenerateTerrainMesh должен быть вызван после того, как загружена карта высот.

// Генерация сетки ландшафта
GenerateTerrainMesh();

Вы можете хранить преобразования ландшафта (перемещение, вращение и масштабирование) внутри класса Terrain, используя созданный в главе 9 класс Transformation. Для этого добавьте к классу Terrain новый атрибут типа Transformation и назовите его transformation. Затем, когда карта высот загружена, вы должны создать новый экземпляр Transformation:

transformation = new Transformation();

И, наконец, вы должны загрузить собственный эффект для ландшафта и инкапсулировать его в объект TerrainEffect. Как говорилось в главе 8, вы должны создавать вспомогательный класс для каждого созданного вами эффекта, что поможет вам управлять параметрами эффекта и модифицировать их. Класс TerrainMaterial — это еще один класс, который вы создаете для конфигурирования эффектов ландшафта:

// Загружаем эффект
effect = new TerrainEffect(
              Game.Content.Load<Effect>(TerrainEffect.EFFECT_FILENAME));
terrainMaterial = new TerrainMaterial();

Собственный эффект, который вы создадите для ландшафта, обеспечит более реалистичную визуализацию с использованием мультитекстурирования и наложения нормалей. Мультитекстурирование позволяет накладывать на одну и ту же поверхность сразу несколько текстур, а наложение нормалей позволяет повысить детализацию ландшафта без увеличения сложности его сетки. Вы создадите эффект, используемый для визуализации ландшафта, в конце этой главы. Ниже показан код метода Load класса Terrain:

public void Load(string heightmapFileName, int vertexCountX, int vertexCountZ,
                 float blockScale, float heightScale)
{
    if (!isInitialized) Initialize();

    this.vertexCountX = vertexCountX;
    this.vertexCountZ = vertexCountZ;
    this.blockScale   = blockScale;
    this.heightScale  = heightScale;

    // Открываем файл карты высот
    FileStream fileStream = File.OpenRead(Game.Content.RootDirectory +
                     "/" + GameAssetsPath.TERRAINS_PATH + heightmapFileName);

    // Читаем данные карты высот
    int heightmapSize = vertexCountX * vertexCountZ;

    heightmap = new byte[heightmapSize];
    fileStream.Read(heightmap, 0, heightmapSize);
    fileStream.Close();

    // Генерируем сетку ландшафта
    GenerateTerrainMesh();

    // Создаем экземпляр преобразования для ландшафта
    transformation = new Transformation();

    // Загружаем эффект
    effect = new TerrainEffect(
                  Game.Content.Load<Effect>(TerrainEffect.EFFECT_FILENAME));
    material = new TerrainMaterial();
}

Метод Load получает в качестве параметров имя файла карты высот; размер ландшафта, выраженный в количестве вершин (по осям X и Z); размер блока, представляющий расстояние между соседними вершинами; и масштабный коэффициент высоты, используемый для масштабирования высоты ландшафта. Все эти параметры, за исключением имени файла карты высот, сохраняются в классе Terrain а атрибутах vertexCountX, vertexCountZ, blockScale и heightScale, соответственно.

Генерация сетки ландшафта

Чтобы сгенерировать сетку ландшафта вам надо сгенерировать ее вершины и индексы. Индексы сетки хранят порядок, в котором должны комбинироваться вершины сетки для образования треугольников. Каждая вершина сетки содержит пространственные координаты и хранит некоторые необходимые для визуализации атрибуты, такие как нормаль и координаты текстуры. Вы должны сгенерировать индексы сетки прежде ее вершин, поскольку некоторые из атрибутов вершин, такие, как нормаль вершины, вы можете вычислить только если знаете, какие вершины используются в каждом треугольнике.

Для генерации индексов и вершин сетки вы создадите два отдельных метода, названных соответственно GenerateTerrainIndices и GenerateTerrainVertices. Вы вызываете эти методы из метода GenerateTerrain, чтобы сгенерировать вершины и индексы сетки. Вы создаете объект XNA VertexBuffer для хранения вершин сетки и объект XNA IndexBuffer для хранения индексов сетки. Буферы вершин и индексов являются буферами в памяти, хранящими свои данные в системной памяти и, по мере необходимости, копирующими их в видеопамять. Для метода GenetateTerrain используется показанный ниже код, вызывающий методы GenerateTerrainIndices и GenerateTerrainVertices для генерации индексов и вершин сетки ландшафта. Вот код для метода GenerateTerrainMesh:

private void GenerateTerrainMesh()
{
    numVertices  = vertexCountX * vertexCountZ;
    numTriangles = (vertexCountX - 1) * (vertexCountZ - 1) * 2;

    // Вы должны сначала генерировать индексы ландшафта
    int[] indices = GenerateTerrainIndices();

    // Затем генерируем вершины ландшафта
    VertexPositionNormalTangentBinormal[] vertices =
                               GenerateTerrainVertices(indices);

    // Создаем буфер вершин для хранения всех вершин 
    vb = new VertexBuffer(GraphicsDevice, numVertices *
                VertexPositionNormalTangentBinormal.SizeInBytes,
                BufferUsage.WriteOnly);
    vb.SetData<VertexPositionNormalTangentBinormal>(vertices);

    // Создаем буфер индексов для хранения всех индексов
    ib = new IndexBuffer(GraphicsDevice, numTriangles * 3 * sizeof(int),
                BufferUsage.WriteOnly, IndexElementSize.ThirtyTwoBits);
    ib.SetData<int>(indices);
}

Обратите внимание, что вершины ландшафта хранятся как массив структур VertexPositionNormalTangentBinormal. Вы должны создать эту вспомогательную структуру для хранения данных вершин потому что вам необходимо хранить местоположение, координаты текстуры, нормаль, касательную и бинормаль каждой вершины, а в XNA нет класса, который хранил бы все эти атрибуты вершины. Вот код для структуры VertexPositionNormalTangentBinormal:

public struct VertexPositionNormalTangentBinormal
{
    public Vector3 Position;
    public Vector3 Normal;
    public Vector2 TextureCoordinate;
    public Vector3 Tanget;
    public Vector3 Binormal;

    public static int SizeInBytes
    {
        get { return (3 + 3 + 2 + 3 + 3) * sizeof(float); }
    }

    public static VertexElement[] VertexElements = new VertexElement[] {
        new VertexElement(0, 0, VertexElementFormat.Vector3,
                          VertexElementMethod.Default,
                          VertexElementUsage.Position, 0),
        new VertexElement(0, 12, VertexElementFormat.Vector3,
                          VertexElementMethod.Default,
                          VertexElementUsage.Normal, 0),
        new VertexElement(0, 24, VertexElementFormat.Vector2,
                          VertexElementMethod.Default,
                          VertexElementUsage.TextureCoordinate, 0),
        new VertexElement(0, 32, VertexElementFormat.Vector3,
                          VertexElementMethod.Default,
                          VertexElementUsage.Tangent, 0),
        new VertexElement(0, 44, VertexElementFormat.Vector3,
                          VertexElementMethod.Default,
                          VertexElementUsage.Binormal, 0)
    };
}

В структуре VertexPositionNormalTangentBinormal есть все необходимые для вершины атрибуты: местоположение, координаты текстуры, нормаль, касательная и бинормаль. В этой структуре также объявлен массив VertexElement, содержащий формат данных вершины, где указаны тип и размер каждого элемента в описании вершины.

Генерация индексов сетки

В этом разделе вы создадите метод GenerateTerrainIndices для генерации индексов сетки ландшафта. Индексы сетки определяют в каком порядке должны комбинироваться вершины для генерации треугольников. На рис. 10.4 показаны индексы вершин в сетке и как они комбинируются для формирования треугольников.


Рис. 10.4. Проиндексированная для создания треугольников сетка вершин

Рис. 10.4. Проиндексированная для создания треугольников сетка вершин


Каждый квадрат ландшафта состоит из двух треугольников: серого и белого. В первом квадрате сетки серый треугольник образуют вершины 0, 1 и 7, а белый треугольник образуют вершины 0, 7 и 6. Заметьте, что порядок индексов треугольника важен: они должны следовать по часовой стрелке, поскольку конвейер визуализации XNA по умолчанию отбрасывает треугольники у которых вершины идут против часовой стрелки.

Обратите внимание на шаблон назначения индексов, используемых для создания треугольников, где индексы первого и второго треугольников каждого квадрата следуют одному и тому же порядку, что показано в следующих формулах:

В показанных формулах значение переменной VertexCountX равно количеству вершин в строке сетки вершин. Используя показанные выше формулы вы можете в цикле перебирать квадраты сетки вершин, генерируя индексы их треугольников. Вы генерируете индексы сетки в виде массива целочисленных значений, в котором для каждого треугольника отведено три элемента. Вот код метода GenerateTerrainIndices:

private int[] GenerateTerrainIndices()
{
    int numIndices = numTriangles * 3;
    int[] indices = new int[numIndices];

    int indicesCount = 0;
    for (int i = 0; i < (vertexCountZ - 1); i++)
    {
        for (int j = 0; j < (vertexCountX - 1); j++)
        {
            int index = j + i * vertexCountZ;

            // Первый треугольник
            indices[indicesCount++] = index;
            indices[indicesCount++] = index + 1;
            indices[indicesCount++] = index + vertexCountX + 1;

            // Второй треугольник
            indices[indicesCount++] = index + vertexCountX + 1;
            indices[indicesCount++] = index + vertexCountX;
            indices[indicesCount++] = index;
        }
    }

    return indices;
}

Генерация местоположения вершин и координат текстуры

В этом разделе вы создадите метод GenerateTerrainVertices для генерации вершин сетки. Вы размещаете вершины ландшафта в мировой плоскости XZ, центрируя ландшафт по мировой позиции (0, 0). Для этого вам сначала надо вычислить половину размера ландшафта по осям X и Z, а затем установить стартовую позицию ландшафта на минус половину его размера по осям X и Z (–halfTerrainWidth, –halfTerrainDepth).

Вы можете вычислить размер ландшафта через его атрибуты: vertexCountX, хранящий количество вершин ландшафта по оси X; vertexCountZ, хранящий количество вершин ландшафта по оси Z; и blockScale, хранящий расстояние между соседними вершинами по осям X и Z. После вычисления размеров ландшафта вам надо просто разделить их на два, как показано ниже:

float terrainWidth = (vertexCountX - 1) * blockScale;
float terrainDepth = (vertexCountZ - 1) * blockScale;
float halfTerrainWidth = terrainWidth * 0.5f;
float halfTerrainDepth = terrainDepth * 0.5f;

Вы генерируете решетку вершин ландшафта, начиная со стартовой позиции ландшафта и проходя по каждой строке решетки вершин, размещает вершины (проходя от –X до +X), переходя затем к следующей линии решетки (проходя от –Z до +Z). Таким образом вершинам решетки назначаются координаты, увеличивающиеся вдоль осей X и Z, согласно заданному вами размеру блока, как показано на рис. 10.2. В ходе размещения вершин вы используете ранее сохраненные данные карты высот, чтобы установить высоту вершины по оси Y. Вы также масштабируете высоту ландшафта, умножая высоту каждой вершины на коэффициент масштабирования: атрибут heightScale класса Terrain. Для корректного размещения вершин в решетке ландшафта можно использовать следующий код:

for (float i = -halfTerrainDepth; i <= halfTerrainDepth; i += blockScale)
    for (float j = -halfTerrainWidth; j <= halfTerrainWidth; j += blockScale)
        Position = (j, heightmap[vertexCount] * heightScale, i)

У каждой вершины также есть координаты текстуры U и V, которые должны изменяться от (0, 0) до (1, 1), где (0, 0) это начальная координата текстуры, а (1, 1) — конечная координата текстуры. На рис. 10.5 показаны координаты текстуры для некоторых вершин в решетке.


Рис. 10.5. Координаты текстуры для вершин решетки (слева). Оси UV на текстуре (справа)

Рис. 10.5. Координаты текстуры для вершин решетки (слева). Оси UV на текстуре (справа)


Чтобы вычислить правильные координаты текстуры для каждой вершины в ландшафте вам сначала нужно вычислить приращение координат текстуры по осям UV. Для этого разделим максимальное значение координаты текстуры (1.0) на количество вершин по каждой из осей минус единица:

float tu = 0; float tv = 0;

float tuDerivative = 1.0f / (vertexCountX - 1);
float tvDerivative = 1.0f / (vertexCountZ - 1);

Затем вы перебираете все вершины, устанавливая их координаты текстуры и увеличивая их. Помимо местоположения и координат текстуры вам надо вычислить нормаль, касательную и бинормаль для каждой вершины. Для этого создадим методы GenerateTerrainNormals и GenerateTerrainTangentBinormal, которые вы вызовете в конце метода GenerateTerrainVertices. Ниже показан полный код метода GenerateTerrainVertices:

private VertexPositionNormalTangentBinormal[] GenerateTerrainVertices(
                                                        int[] terrainIndices)
{
    float halfTerrainWidth = (vertexCountX - 1) * blockScale * 0.5f;
    float halfTerrainDepth = (vertexCountZ - 1) * blockScale * 0.5f;

    // Координаты текстуры
    float tu = 0;
    float tv = 0;

    float tuDerivative = 1.0f / (vertexCountX - 1);
    float tvDerivative = 1.0f / (vertexCountZ - 1);

    int vertexCount = 0;

    // Создаем массив вершин
    VertexPositionNormalTangentBinormal[] vertices =
          new VertexPositionNormalTangentBinormal[vertexCountX * vertexCountZ];

    // Устанавливаем местоположение и координаты текстуры каждой вершины 
    for (float i = -halfTerrainDepth; i <= halfTerrainDepth; i += blockScale)
    {
        tu = 0.0f;
        for (float j = -halfTerrainWidth; j <= halfTerrainWidth; j += blockScale)
        {
            // Устанавливаем местоположение вершины и UV
            vertices[vertexCount].Position =
                     new Vector3(j, heightmap[vertexCount] * heightScale, i);
            vertices[vertexCount].TextureCoordinate = new Vector2(tu, tv);
            tu += tuDerivative;
            vertexCount++;
        }
        tv += tvDerivative;
    }

    // Генерируем нормали, касательные и бинормали вершин 
    GenerateTerrainNormals(vertices, terrainIndices);
    GenerateTerrainTangentBinormal(vertices, terrainIndices);

    return vertices;
}

Генерация нормалей вершин

Вектор нормали каждой вершины треугольника равен вектору нормали треугольника. Итак, чтобы вычислить нормали вершин треугольника вам надо вычислить нормаль треугольника. Вы вычисляете нормаль треугольника, находя векторное произведение между двумя векторами, формируемыми из его вершин, (v1 – v0) и (v2 – v0), поскольку векторное произведение возвращает вектор перпендикулярный этим двум векторам.

Поскольку в решетке вершин отдельную вершину могут совместно использовать от одного до шести треугольников, нормаль каждой вершины является суммой нормалей тех треугольников, которые совместно используют эту вершину. Следовательно, вам необходимо вычислить векторы нормали для каждого треугольника и просуммировать их, чтобы получить нормаль вершины этих треугольников. В конце вы должны нормализовать нормаль каждой вершины, чтобы она была единичной длины. Векторы нормали используются в расчетах освещения, и чтобы в результате получить правильное освещение они должны быть единичной длины. Используйте следующий код для метода GenerateTerrainNormal, генерирующего нормали вершин ландшафта:

private void GenerateTerrainNormals(VertexPositionNormalTangentBinormal[] vertices,
                                    int[] indices)
{
    for (int i = 0; i < indices.Length; i += 3)
    {
        // Получаем местоположение вершин (v1, v2 и v3)
        Vector3 v1 = vertices[indices[i]].Position;
        Vector3 v2 = vertices[indices[i + 1]].Position;
        Vector3 v3 = vertices[indices[i + 2]].Position;

        // Вычисляем векторы v1->v3 и v1->v2 и нормаль,
        // как результат векторного произведения
        Vector3 vu = v3 - v1;
        Vector3 vt = v2 - v1;
        Vector3 normal = Vector3.Cross(vu, vt);
        normal.Normalize();

        // Складываем эту нормаль с текущей нормалью трех вершин
        vertices[indices[i]].Normal += normal;
        vertices[indices[i + 1]].Normal += normal;
        vertices[indices[i + 2]].Normal += normal;
    }

    // После вычисления всех нормалей нормализуем их
    for (int i = 0; i < vertices.Length; i++)
        vertices[i].Normal.Normalize();
}

Генерация касательных и бинормалей вершин

Собственный эффект, который вы создадите для ландшафта, использует технику, называемую наложение нормалей (normal mapping), позволяющую увеличить детализацию ландшафта без увеличения сложности его сетки. Чтобы использовать технику наложения нормалей, у каждой вершины сетки должны быть векторы касательной, бинормали и нормали. Векторы касательной, бинормали и нормали взаимно перпендикулярны и образуют касательную (или тангенциальную) систему координат. На рис. 10.6 показаны векторы касательной, бинормали и нормали для различных точек двух разных поверхностей.


Рис. 10.6. Векторы касательной (T), бинормали (B) и нормали (N)

Рис. 10.6. Векторы касательной (T), бинормали (B) и нормали (N)


Вы можете вычислить вектор касательной каждой вершины в решетке вершин, как вектор, начинающийся в этой вершине и заканчивающийся в следующей вершине решетки. Таким образом, векторы касательной будут параллельны оси X решетки. Заметьте, что вектор касательной последней вершины в строке решетки вычисляется как вектор, начинающийся в предпоследней вершине строки и заканчивающийся в последней вершине.

После вычисления вектора касательной вы можете получить вектор бинормали, выполнив векторное умножение между касательной и нормалью вершины. Рис. 10.7 показывает векторы касательных, бинормалей и нормалей плоской решетки вершин.


Рис. 10.7. Векторы касательных (T), бинормалей (B) и нормалей (N) некоторых вершин плоской решетки

Рис. 10.7. Векторы касательных (T), бинормалей (B) и нормалей (N) некоторых вершин плоской решетки


Используйте следующий код метода GenerateTerrainTangentBinormal для вычисления векторов касательной и бинормали вершин:

public void GenerateTerrainTangentBinormal(
                        VertexPositionNormalTangentBinormal[] vertices,
                        int[] indices)
{
    for (int i = 0; i < vertexCountZ; i++)
    {
        for (int j = 0; j < vertexCountX; j++)
        {
            int vertexIndex = j + i * vertexCountX;
            Vector3 v1 = vertices[vertexIndex].Position;

            // Вычисляем вектор касательной
            if (j < vertexCountX - 1)
            {
                Vector3 v2 = vertices[vertexIndex + 1].Position;
                vertices[vertexIndex].Tanget = (v2 - v1);
            }
            // Особый случай: последняя вершина плоскости по оси X
            else
            {
                Vector3 v2 = vertices[vertexIndex - 1].Position;
                vertices[vertexIndex].Tanget = (v1 - v2);
            }

            // Вычисляем бинормаль как векторное происведение
            // (Касательная х Нормаль)
            vertices[vertexIndex].Tanget.Normalize();
            vertices[vertexIndex].Binormal = Vector3.Cross(
            vertices[vertexIndex].Tanget, vertices[vertexIndex].Normal);
        }
    }
}

netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz