вторник, 1 марта 2016 г.

О ландшафте серьезно. Подведение итогов.

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

1. Берем плейн, обыкновенный Plane. Подразделяем его (W - Subdivide) на квадраты. Отмечу, что заранее надо примерно представить себе размеры террайна по Х и Y и сам террайн должен быть квадратным в этом плане.  Также заранее надо рассчитать, сколько блоков-квадратов необходимо получить по сторонам террайна - то есть знать величину блока террайна, сам блок должен быть тоже квадратным.
2. Получив требуемый уровень подразделения, ставим размеры заготовки будущего террайна в стандартные рамки. Изначально в Блендере добавляемые плейны имеют размер 2х2х0 метров и Scale 1х1х1. Поэтому полученная заготовка должна быть именно таких размеров - узнано на собственном горьком опыте, если имеются другие размеры - будут искажения при дальнейшей работе. Когда все размеры подогнаны, нажимаем U - Unwrap и проверяем раскладку вершин на УВ-развертке - она должна быть равномерной  - просто квадрат в "мелкую клеточку", не более.
3. Выделяем все вершины заготовки и жмем Ctrl-E - Edge Split.  Происходит разделение выделенных ребер заготовки, но она сама еще представляет из себя единое целое (это очень важно- в этом шаге закладывается основа для дальнейшего "дробления" террайна на блоки).
4. Далее опять выделяем все вершины заготовки и снова применяем W - subdivide. Здесь уже вам решать, насколько будет сложен и детализирован террайн. У меня комп выдерживал 640 тысяч полигонов, но для страховки после каждого шага я сохранял файл, потому что вылеты Блендера  редкостью уже не были.
5. Берем карту высот, заранее подготовленную в ГИМПЕ - какого формата и размера - неважно, главное, чтобы комп выдержал следующую операцию. В Блендере есть такой отличный инструмент, как Displace в модификаторах, который позволяет создать модель на основе карты высот - очень быстро и вполне качественно. Настройки модификатора - на ваше усмотрение - в зависимости от требуемого результата. После нажатия Аpply получаем мини-террайн размерами 2 на 2 метра и очень сильно детализированный. Сохраняем файл.
6. Над террайном подвешиваем камеру (повыше, этак км на 500-700 (да именно так высоко, не забудьте ввести поправки в дальность видения для камеры). Переключаемся в вид из этой камеры. Увеличиваем ландашафт до требуемых размеров по всем осям. У меня размеры террайна - 1000 на 1000 км, поэтому и высота камеры такая, если у вас размеры меньше, камеру можно высоко не поднимать, но ваш ландшафт должен быть видимым целиком!). Далее добавляем материалы и текстуры к ним к нашему террайну.
7. Постоянно после каждого шага сохраняем результат во избежание вылета Блендере и потери результатов. Далее применяем к ландшафту нодовый материал - он позволяет дешево и сердито за котороткое время получить вполне приемлемый результат. Как это делается - смотрим здесь:
http://b3d.org.ua/forum/viewtopic.php?f=30&t=571 Мой коллега denis8424  расстарался. Ноды вообще вещь хорошая, жаль что нет времени для их подробного изучения... Тем более, что в последней версии БГЕ можно некоторые параметры для нодов задавать скриптами.
8. После получения "раскрашенного" террайна начинается самое нудное. Приготовьтесь подождать. Перейдем в режим редактирования меша, выдели все вершины и жмем P - loose party. Оставляем комп в покое - и ничего нетрогаем. Желательно в это время вообще к компу не приставать - у него и без вас заботы хватит. Ландшафт режется на отдельные блоки вдоль уже порезанных нами ребер (см. выще щаг 3). Когда операция завершается, перед вами множество объектов-блоков. Да, забыл написать, что перед разрезанием на блоки назовите свой ландшафт как-то вроде Terrain.000. Получившиеся блоки будут иметь схожие названия, отличающиеся только цифрами.
9. Теперь нам крайне необходимо привести в соотетствие имена мешей и объектов - они должны совпадать и дать каждому блоку свой центр, отличный от нуля - так будет меньше нагрузка на БГЕ. Для этого я применял скрипт

 import bpy

scene = bpy.context.scene

for ob in scene.objects:
    if ob.layers[scene.active_layer] == True and ob.name != 'Camera':
        if 'BigDesert' in ob.name:
               
            ob.name = 'BigDesert'
            ob.data.name = ob.name

В данном скрипте просто производится поиск по части имени блока (у вас может быть какое угодно, вместо BigDesert, но при создании серии ландшафтов лучше использовать что-то одинаковое - не будете же вы грузить сразу пару-тройку разных террайнов).
Для выправления геометрии, в смысле создания новых центров объектов применяется второй скрипт:
 import bpy

scene = bpy.context.scene
for obj in scene.objects:
    if 'Terrain' in obj.name: # здесь может быть другое имя
        obj.select = True
        obj.data.name = obj.name
        bpy.ops.object.origin_set(type = 'ORIGIN_GEOMETRY', center = 'MEDIAN')
        scene.cursor_location = obj.location
        scene.cursor_location[2] = 0.0
        bpy.ops.object.origin_set(type = 'ORIGIN_CURSOR')
        obj.location[2] = 0.0
        obj.select = False
Оба скрипта запускаются с помощью RunScript в текстовом редакторе Блендера.
Можно считать, что ландшафт создан. можно встраивать его в игру.

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

import bge
cont = bge.logic.getCurrentController()
scene = bge.logic.getCurrentScene()
own = cont.owner

listCoord = [-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29,-27,-25,-23,-21,-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49]

listIndex = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49]

for obj in scene.objects:
    if 'Terrain' in obj.name:
       
        X, Y, Z = obj.worldPosition
       
        try:
            for coordX in listCoord:
                if  (coordX-1)*10000 < X < (coordX+1)*10000:
                    blockX = listCoord.index(coordX)
               
            for coordY in listCoord:
                if  (coordY-1)*10000 < Y < (coordY+1)*10000:
                    blockY = listCoord.index(coordY) 
       
       
            coordText = open(bge.logic.expandPath('//BlockTerrain_2.txt'),'a')
            coordText.write('b|' + str(blockX) + '|' + str(blockY) + '|' + obj.name + '|'+ str(X)+'|'+str(Y) +'|'+ str(Z)+'|'+'\n')
            coordText.close()
        except:
            pass

После отработки скрипта заглядываем в наш текстовый файл с данными. Там будет запись типа:

b|30|29|Terrain.2499|110000.0|90000.0|0.0|
b|45|29|Terrain.2498|410000.4375|90000.0|0.0|
b|46|29|Terrain.2497|430000.4375|90000.0|0.0|
b|45|13|Terrain.2496|410000.4375|-230000.515625|0.0|
b|46|13|Terrain.2495|430000.4375|-230000.515625|0.0|
b|13|29|Terrain.2494|-230000.546875|90000.0|0.0|

Довольно исчерпывающие данные, квадрат такой-то, имя блока террайна, его координаты. Можно писать класс для генерации блоков террайна, причем нужных в данный момент.
2. Написание класса много времени у опытных людей не займет. я вроде бы уж и научился писать, но ляпы все же делаю и с первого захода у меня не получилось. После правки у меня получилось следующее:

# -*- coding: utf8 -*-

import bge
scene = bge.logic.getCurrentScene()  
cont = bge.logic.getCurrentController()
own = cont.owner
#Загрузка выбранного ландшафта - временно закомменченная
terrain = bge.logic.globalDict['terrain']
         
class terrain(bge.types.KX_GameObject):   
    def __init__(self, old_owner):   
      
        #Список необходимых в данный момент блоков
        self.listBlock = []
   
        #Вычисление нужного квадрата, центрального блока ландшафта под камерой, целочисленные значения
        #20 - размер блока в км, 490 - поправка сдвиг влево и вниз на "начало отсчета" от левого нижнего угла
        #1000 - чтобы было меньше возни, отбрасываем метры, нас интересуют целые числа в диапазоне от 0 до 49
        activeCam = scene.active_camera
        quadroX = int((int(activeCam.worldPosition[0]/1000)+ 490)/20)
        quadroY = int((int(activeCam.worldPosition[1]/1000)+ 490)/20)
       
        #Отыскиваем и открываем нужный нам текстовый файл с названиями, координатами и значениями квадратов для блоков ландшафта
        #print('//Terrain/' + terrain + '_/Block' + terrain + '.txt')
        #brikeLand = open(bge.logic.expandPath('//Terrain/' + terrain + '_/Block' + terrain + '.txt'),'r')
        brikeLand = open(bge.logic.expandPath('//Terrain/Terrain_2_/BlockTerrain_2.txt'),'r')
   
        #Дальше по маркерам смотрим, что нам нужно
        for string in brikeLand:
            if string[0] == '#':
                continue
            elif string[0] == 'b':
                tempList = string.split('|')
                X = int(tempList[1])
                Y = int(tempList[2])
           
                #Вычисляем индексы квадратов, нужных для вызова
                #Если индексы квадрата по ХУ находятся внутри требуемого диапазона - добавляем название блока ландшафта в список
                #нужных для появления блоков
                if quadroY-2 < Y < quadroY+2 and quadroX-2 < X < quadroX+2:
                    self.listBlock.append(tempList[3])
                   
                    #Обратная проверка - если объекты есть в списке, но их нет в сцене - добавляем (не хватало еще дублей наплодить)
                    for blockLand in self.listBlock:
                        if blockLand not in scene.objects:
                            blockLand = scene.addObject(tempList[3], self)
                            blockLand.worldPosition = [float(tempList[4]), float(tempList[5]), float(tempList[6])]
                           
            #Проверка на наличие лишних блоков ландшафта в сцене
            for landBlock in scene.objects:
                if "Terrain" in landBlock.name:
                    if landBlock.name not in self.listBlock:
                        #Если есть таковые - ликвидируем
                        landBlock.endObject()
                       
        #Закрываем текст с данными террайна           
        brikeLand.close()
       
def mutate(cont):
    old_object = cont.owner
    mutated_object = air(cont.owner)
   
    assert(old_object is not mutated_object)
    assert(old_object.invalid)
    assert(mutated_object is cont.owner)

# Called later - note we are now working with the mutated object.
def update(cont):
    cont.owner.update()

Комментарии на английском - это просто невырезанные строчки из исходных примеров, приведенных в АПИ Блендера. Можете смело удалять. Думаю русские комментарии вполне понятны. И зачем нам понадобился здоровенный список с именами блоков и их данными теперь тоже понятно.
3. Само использование класса в игре. Сами по себе классы  экономят ресурсы и упрощают жизнь (скрипт класса блоков террайна я разместил в пусковом файле игры - это стандартная вещь для всех террайнов). Далее у меня получился террайн из 2500 блоков, размером 1000 на 1000 км, по 50 блоков по Х и Y, в сумме террайн имеет 640 тысяч поликов, каждый его блок имеет размер 20 на 20 км и 256 поликов. В игре надо вызывать 9 или максимум 25 блоков  -образуя видимую камерой часть ландшафта. Для подгрузки всего террайна использовался инструмент LibLoad, о его использовании информации довольно много, так что разберетесь. Сам процесс вызова класса происходит лишь при смене камеры или в строго определенные моменты времени (тут надо рассчитывать в зависимости от скорости перемешения активной камеры). Пока что скрипт работы генерации блоков террайна написан лишь для клавиш ф1-12 (смена камеры),  но общий принцип, думаю, вы поймете. Не забудьте заодно - блоки ландшафта в подгружаемом файле должны быть на неактивном слое!

# -*- coding: utf8 -*-
import bge

def keyButton():
    scene = bge.logic.getCurrentScene()
    cont = bge.logic.getCurrentController()
    own = cont.owner
    sens = cont.sensors['keyButtonCam']
   
    if sens.positive:
       
        for key,status in sens.events:
            #Клавиши переключения камер
            if status == bge.logic.KX_INPUT_JUST_ACTIVATED:
                #Импорт класса террайн - для замены или подгрузки блоков террайна
                if key == bge.events.F1KEY or key == bge.events.F2KEY or key == bge.events.F3KEY or key == bge.events.F4KEY or key == bge.events.F5KEY or key == bge.events.F6KEY or key == bge.events.F7KEY or key == bge.events.F8KEY or key == bge.events.F9KEY or key == bge.events.F10KEY or key == bge.events.F11KEY or key == bge.events.F12KEY:
                    import ClassTerrain
                    blockTerrain = ClassTerrain.terrain(own)

Все. Ландшафт создан, затекстурен, порезан на кусочки, проименован, "взвешен и учтен". Он подключен к игре, используется только нужная в данный момент его часть. Есть еще нюансы насчет использования блоков, на которых ведет бой наземная техника, они должны также присутствлвать изначально, чтобы техника "не проваливалась", но эта задача вполне решаема. Основная часть работы проделана и описана здесь. Заодно может пригодиться не только всем желающим, но и мне, "на всякий пожарный". Сделал, что мог, пусть другие сделают лучше (с). Выражаю благодарность denis8424 за терпение и помощь в написании скриптов-инструментов и вылавливании моих косяков.

Комментариев нет:

Отправить комментарий