Добавляем в компиляторы поддержку -subdivide 256

Материал из CSM Wiki
Перейти к навигации Перейти к поиску

Как известно, компилятор hlbsp поддерживает опцию –subdivide. С ее помощью можно задать значение (в юнитах) частоты разбиения поверхностей при построении BSP-дерева уровня. Работает это следующим образом. Поверхности брашей, соприкасающиеся друг с другом, имеющие одну и ту же текстуру и выравнивание, склеиваются друг с другом. Даже отдельные, не склеенные поверхности, могут достигать огромных размеров – например, левел-дизайнер сделал карту-коробку размером чуть ли не во всё доступное в VHE пространство. Такие поверхности компилятор разбивает на множество мелких, и делает это таким образом, чтобы максимальный линейный размер поверхности (в направлении текстурных осей) не превышал значение 240. Эти 240 юнитов берутся не в мировых, а в текстурных координатах. Иными словами, если на полигон 256x256 наложена текстура со скейлом 1.0 по вертикали и горизонтали, то ее линейные размеры 256х256 юнитов. А если скейл равен 2.0, т.е. текстура растянута, то размеры будут равны 128х128. Формула расчета линейного размера полигона выглядит так:

x_size = poly_width / HScale;
y_size = poly_height / VScale;

Параметр –subdivide позволяет задать другое, отличное от 240, значение. Зачем это делать? При задании меньшего значения компилятор попросту будет создавать больше полигонов, таким образом увеличивая r_speeds итоговой карты. А если задать значение больше, то значит, полигонов создаваться будет меньше, и r_speeds снизится. Среди левел-дизайнеров весьма распространены текстуры размером 256х256 и 512х512. Это не случайно – такие текстуры позволяют максимально эффективно использовать видеопамять, т.к. размеры загружаемых текстур приводятся к ближайшей степени двойки с округлением в большую сторону. Т.е. текстура 240х240 в видеопамяти все равно увеличится до 256х256, но будет содержать меньше деталей за счет потерянных 16 пикселей по вертикали и горизонтали. Зато сторона браша, подогнанного под размер текстуры 240х240, дает при разбиении один полигон (т.е. не разбивается вообще), а подогнанного под 256х256 – аж четыре полигона, увеличивая r_speeds «на пустом месте». Поэтому было бы удобно иметь возможность компилировать карты с параметром –subdivide, равным 512, ну или хотя бы 256. И вот тут нас подстерегает серьезное ограничение архитектуры движка Half-Life. Оказывается, накладываемая на полигон карта освещенности (лайтмапа) должна быть такой, чтобы каждый ее пиксель покрывал ровно 16х16 пикселей оригинальной текстуры! Т.е. накладывается она со скейлом 16.0. А максимальный размер одной лайтмапы не может превышать 16 пикселей по ширине или высоте. Простой расчет показывает, что максимальный размер полигона, на который можно наложить лайтмапу, равен 256. Если же он больше, то возникает знакомая многим ошибка Bad Surface Extents – она связана именно с тем, что полигон не удается полностью «накрыть» лайтмапой. Итак, о разбиении 512 и больше можно смело забыть. Но, значит, можно использовать значение 256? И да, и нет. Компилируя с этим параметром, вы всё равно можете получить ошибку Bad Surface Extents при компиляции hlrad-ом. Это связано с тем, что округление линейных размеров идет в большую сторону. А так как при разбиении часто получаются ошибки округлений, то полигон размером 256х256 может превратиться в полигон размером 256,01х256,01 и таким образом превысит лимит. От этого можно избавиться, исправив функцию в hlrad – тогда компилятор будет «пропускать» такие полигоны, считая их правильными. В результате карта загрузится игрой… но только в Hardware-режиме (OpenGL или Direct3D). При попытке запустить ее в режиме Software или в режиме выделенного сервера (HLDS) мы получим всё ту же ошибку Bad Surface Extents. Но тут мы уже не можем залезть в исходники и поправить округление – исходников-то нет! Поэтому воспользуемся другой тактикой – будем проверять размеры на этапе разбиения в hlbsp, и в случае превышения проводить дополнительное разбиение. Итог печален – все наши полигоны 256,01х256,01 будут разбиты на 4 полигона, и выигрыш в r_speeds станет заметно скромнее. Но зато карта будет работать во всех режимах, в т.ч. на выделенном сервере. Работать мы будем в проекте hlbsp. Интересующая нас функция называется SubdivideFace и находится в файле surfaces.cpp. Откройте его и после строки

static int      subdivides;

добавьте:

int c_subdivideFix = 0;
int c_subdivideTotal = 0;

Сразу после этого добавим функцию расчета линейных размеров полигонов. Она работает точно так же, как функции в движке и в hlrad (хотя и несколько проще последней).

static int CalcFaceExtents(face_t* f, int axis)
{
    vec_t           mins, maxs, val;
    int             i;
    texinfo_t*      tex;
    
    mins = 999999;
    maxs = -99999;
    
    tex = &g_texinfo[f->texturenum];
    
    for (i = 0; i < f->numpoints; i++)
        {
            val = f->pts[i][0] * tex->vecs[axis][0] +
        f->pts[i][1] * tex->vecs[axis][1] + f->pts[i][2] * tex->vecs[axis][2] + tex->vecs[axis][3];
        
        if (val < mins)
            {
                mins = val;
        }
        if (val > maxs)
            {
                maxs = val;
        }
    }
    
    mins = floor(mins / 16.0);
    maxs = ceil(maxs / 16.0);
    
    return maxs - mins;
}

Теперь перепишем фукнцию SudivideFace так, чтобы она учитывала размеры и выполняла дополнительное разбиение, если потребуется. Итоговый вариант выглядит так:

void SubdivideFace(face_t* f, face_t** prevptr)
{
    vec_t           mins, maxs;
    vec_t           v;
    int             axis;
    int             i;
    dplane_t        plane;
    face_t*         front;
    face_t*         back;
    face_t*         next;
    texinfo_t*      tex;
    vec3_t          temp;
    
    // special (non-surface cached) faces don't need subdivision
    
    tex = &g_texinfo[f->texturenum];
    
    if (tex->flags & TEX_SPECIAL)
        {
            return;
    }
    
    if (f->facestyle == face_hint)
        {
            return;
    }
    if (f->facestyle == face_skip)
        {
            return;
    }
    
#ifdef ZHLT_NULLTEX    // AJM
    if (f->facestyle == face_null)
        return; // ideally these should have their tex_special flag set, so its here jic
#endif
    
    for (axis = 0; axis < 2; axis++)
        {
            while (1)
                {
                    int test_subdivide_size = g_subdivide_size;
                
            mins = 999999;
            maxs = -999999;
            
            for (i = 0; i < f->numpoints; i++)
                {
                    v = DotProduct(f->pts[i], tex->vecs[axis]);
                if (v < mins)
                    {
                        mins = v;
                }
                if (v > maxs)
                    {
                        maxs = v;
                }
            }
            
            if ((maxs - mins) <= test_subdivide_size)
            {
                c_subdivideTotal++;
                
                int extents = CalcFaceExtents( f, axis );
                
                if (extents > 16)
                {
                    if (test_subdivide_size > 256)
                        test_subdivide_size >>= 1;
                    else
                        test_subdivide_size -= 16;
                }
                else
                {
                    if ((test_subdivide_size > 240) && ((maxs - mins) > 240) )
                    {
                        c_subdivideFix++;
                    }
                    
                    break;
                }
            }
            
            // split it
            subdivides++;
            
            VectorCopy(tex->vecs[axis], temp);
            v = VectorNormalize(temp);
            
            VectorCopy(temp, plane.normal);
            plane.dist = (mins + test_subdivide_size - 16) / v;
            next = f->next;
            SplitFace(f, &plane, &front, &back);
            if (!front || !back)
                {
                    Developer(DEVELOPER_LEVEL_SPAM, "SubdivideFace: didn't split the %d-sided polygon @(%.0f,%.0f,%.0f)",
                f->numpoints, f->pts[0][0], f->pts[0][1], f->pts[0][2]);
                break;
            }
            *prevptr = back;
            back->next = front;
            front->next = next;
            f = back;
        }
    }
}

Добавим вывод дополнительной информации о том, насколько эффективно наш «фикс» работает. Откроем qbsp.cpp и найдем функцию ProcessFile. Прямо перед ней добавим ссылки на глобальные переменные, которые мы объявили в surfaces.cpp:

extern int c_subdivideFix;
extern int c_subdivideTotal;

Теперь в коде функции ProcessFile сразу после строк:

#ifdef HLBSP_THREADS // AJM
NamedRunThreadsOnIndividual(nummodels, g_estimate, ProcessModel);
#else
// process each model individually
while (ProcessModel())
    ;
#endif

добавим:

Log("%i/%i faces skipped from subdivision\n", c_subdivideFix, c_subdivideTotal);

Теперь в логе будет видна информация о разбиении. Первое число – количество поверхностей, для которых наш увеличенный –subdivide был игнорирован, а второе – общее число поверхностей, подвергавшихся разбиению. Чем меньше первое число по отношению ко второму – тем больше экономия полигонов и тем ниже итоговый r_speeds. Этот код написан так, что отныне параметр –subdivide может принимать любые значения, даже 512 и 1024. Но как нетрудно убедиться, в этом случае все полигоны будут разбиваться и выигрыш будет нулевой. Причина описана выше. А в случае использования –subdivide 256 вполне реально получить выигрыш. Насколько большой – зависит от геометрии карты, и предугадать его сложно. Описанный метод оптимизации карты использовался мной на карте fy_vadrigar, где продемонстрировал неплохие результаты, сэкономив в целом не менее 100 пунктов r_speeds. Пример оптимизации – на скриншоте (сделан в Software-режиме с включенным r_drawflat). Справа - изображение карты, скомпилированной с -subdivide 256, слева - с -subdivide 240 (стандарт).

Subdivide-240-256.jpg