Мипмэппинг
Если полигон относительно сильно удален или повернут, так, что соседним пикселам на экране соотвествуют сильно разнесенные точки текстуры, то возникают всякие неприятные артефакты - можно считать, что потому, что при текстурировании мы выбираем лишь какую-то одну точку текстуры, а реально в экранный пиксел будет проецироваться несколько текселов (точек текстуры).Вообще идеальным методом было бы следующее: провести до пересечения с гранью 3D-пирамиду с вершиной в камере и основанием-пикселом, выбрать все точки текстуры, попадающие в наш пиксел, и усреднить значения их цветов. Вот только вычислительные затраты на одну точку в этом случае окажутся просто фантастическими.
Поэтому для удаления артефактов используется значительно более простая вещь, а именно мипмэппинг. Идея, как обычно, проста. Для каждой текстуры заранее создается несколько ее копий уменьшенного размера (1/2, 1/4, и так далее), а далее при текстурировании используется либо сама текстура, либо подходящая уменьшенная копия. Памяти при этом расходуется на 25-33% больше, чем без мипмэппинга, но зато, вроде бы, увеличивается качество изображения.
Как создать уменьшенную в два раза копию текстуры? Здесь мы опишем три метода, два из них очевидны, третий позаимствован у Crystal Space. Методы расположены в порядке уменьшения скорости и увеличения качества уменьшенной текстуры.
Метод 1. Выкинуть все пикселы текстуры с нечетными координатами. Самый простой, самый быстрый, но дает не очень хорошо выглядящие результаты.
Метод 2. Оставить точки с четными координатами, в каждой точке усреднить значения цвета в этой точке и ее трех соседях (справа, снизу и справа-снизу).
Метод 3. Оставить точки с четными координатами, использовав в каждой точке фильтр, заданный вот такой матрицей:
[ 1 2 1 ]
1/16 * [ 2 4 2 ]
[ 1 2 1 ]
В виде формул для каждой из компонент цвета точки уменьшенной в два раза копии текстуры эти методы запишутся, соответственно, так:
mip1[x][y] = tex[2*x][2*y]; // метод 1
mip2[x][y] = ( // метод 2
tex[2*x ][2*y ] +
tex[2*x+1][2*y ] +
tex[2*x ][2*y+1] +
tex[2*x+1][2*y+1]) / 4;
mip3[x][y] = ( // метод 3
1 * tex[2*x-1][2*y-1] +
2 * tex[2*x ][2*y-1] +
1 * tex[2*x+1][2*y-1] +
2 * tex[2*x-1][2*y ] +
4 * tex[2*x ][2*y ] +
2 * tex[2*x+1][2*y ] +
1 * tex[2*x-1][2*y+1] +
2 * tex[2*x ][2*y+1] +
1 * tex[2*x+1][2*y+1]) / 16;
Последовательно применяя любой из описанных методов, мы можем построить набор уменьшенных текстур. Остается выяснить, какую именно из них надо выбрать при текстурировании. Здесь опять будет описано два достаточно простых метода; а вообще, конечно, их можно придумать значительно больше.
Метод 1: полигональный мипмэппинг. В этом случае мы считаем площадь полигона на экране в пикселах и его же площадь в текстуре в текселах (последнюю обычно можно посчитать заранее), определяем по ним примерное количество пикселов, соотвествующих одному пикселу и выбираем нужный уровень уменьшения текстуры по следующей формуле:
miplevel = floor(log2(screenArea / textureArea) / 2);
здесь
screenArea - площадь грани на экране (в пикселах)
textureArea - площадь грани в текстуре (в текселах)
log2() - функция двоичного логарифма (для Watcom C стандартная)
miplevel - уровень уменьшения; выбираемая текстура должна быть сжата
по обеим осям в (2^miplevel) раз
Поскольку бесконечное количество уменьшенных копий текстуры никто хранить не будет, да и увеличенные текстуры тоже обычно не хранят, а miplevel может получится любым действительным числом, надо, конечно, поставить заглушку:
miplevel = floor(log2(screenArea / textureArea) / 2);
if (miplevel < 0) miplevel = 0;
if (miplevel > MAXMIPLEVEL) miplevel = MAXMIPLEVEL;
screenArea и textureArea проще всего, по-моему, посчитать по формуле Герона для площади треугольника:
// a, b, c - стороны треугольника; p - периметр
a = sqrt((v2.sx-v1.sx)*(v2.sx-v1.sx) + (v2.sy-v1.sy)*(v2.sy-v1.sy));
b = sqrt((v3.sx-v1.sx)*(v3.sx-v1.sx) + (v3.sy-v1.sy)*(v3.sy-v1.sy));
c = sqrt((v3.sx-v2.sx)*(v3.sx-v2.sx) + (v3.sy-v2.sy)*(v3.sy-v2.sy));
p = (a + b + c);
screenArea = sqrt(p * (p-a) * (p-b) * (p-c));
a = sqrt((v2.u-v1.u)*(v2.u-v1.u) + (v2.v-v1.v)*(v2.v-v1.v));
b = sqrt((v3.u-v1.u)*(v3.u-v1.u) + (v3.v-v1.v)*(v3.v-v1.v));
c = sqrt((v3.u-v2.u)*(v3.u-v2.u) + (v3.v-v2.v)*(v3.v-v2.v));
p = (a + b + c);
textureArea = sqrt(p * (p-a) * (p-b) * (p-c));
Этот метод практически не требует вычислительных затрат, так как все операции проделываются один раз на грань. С другой стороны, здесь использутся один и тот же уровень уменьшения (он же уровень детализации, LOD, level of detail) для всего полигона, а разным пикселам может соответствовать разное количество текселов. Есть и более неприятное следствие - уровни уменьшения для двух соседних полигонов меняются скачком, а это не очень хорошо выглядит.
Метод 2: попиксельный мипмэппинг. В этом случае нужный уровень уменьшения считается для каждого пиксела и выбирается на основе максимального шага в текстуре из соответствующих переходу к соседнему пикселу:
textureStep = max(
sqrt(dudx * dudx + dvdx * dvdx),
sqrt(dudy * dudy + dvdy * dvdy));
miplevel = floor(log2(textureStep));
Подобную операцию для каждого пиксела проводить, конечно, накладно. Но при аффинном текстурировании dudx, dvdx, dudy и dvdy постоянны для всех пикселов, так что попиксельный мэппинг становится полигонным, только с другой методикой расчета уровня уменьшения. Для перспективно-корректного же текстурирования dudx, dvdx, dudy и dvdy постоянны для всех пикселов одного кусочка (span'а), так что уровень уменьшения считается раз в несколько пикселов.
Впрочем, даже раз в несколько пикселов подобное (два корня и один логарифм) считать будет достаточно медленно. Поэтому займемся небольшой оптимизацией: во-первых, для скорости можно сделать упрощение и считать, что
textureStep = sqrt(dudx * dudx + dvdx * dvdx);
Далее, заметим, что log2(sqrt(x)) = log2(x) / 2, откуда
miplevel = floor(log2(dudx * dudx + dvdx * dvdx) / 2);
Осталась, практически, одна трудоемкая операция - взятие логарифма. Но и ее можно убрать. Дело в том, что числа с плавающей запятой (float'ы) как раз и хранятся в логарифмической форме, и floor(log2(x)) можно посчитать вот так:
float x;
int floor_log2_x;
x = 123456;
floor_log2_x = ((*((int*)&x)) - (127 << 23)) >> 23; // чистый C
floor_log2_x = (((int&)x) - (127 << 23)) >> 23; // C++
Соответственно, floor(log2(sqrt(x))) = floor(log2(x) / 2) считаем как
miplevel = ((*((int*)&x)) - (127 << 23)) >> 24; // чистый C
miplevel = (((int&)x) - (127 << 23)) >> 24; // C++
Естественно, что этот трюк можно применить и в случае полигонного мипмэпинга для полного устранения всяческих медленых операций типа sqrt(), log2(). Вот, в общем-то, и все.
Параболическое текстурирование, Билинейная фильтрация текстур
Этот метод основан на приближении u, v квадратичными функциями - параболами, то есть. Для каждой строки строится приближающие u, v квадратичные функции, дальше с их помощью они интерполируются по строке. Для этого нам понадобятся точные значения u, v в трех точках - начале, середине и конце строки. Их считаем точно так же, как в 4.3.
Итак, пусть у нас есть точные значения u в начале, середине и конце строки, то есть на расстоянии 0, length/2 и length пикселов от начала этой строки, обозначим их как ua, ub, и uc соответственно. Мы пытаемся приблизить u квадратичной функцией, то есть полагаем, что
u = A*x*x + B*x + C,
где x - расстояние от текущей точки до начала строки. Тогда, подставив в формулу ua, ub, uc и соответствующие им x, получаем:
ua = C,
ub = A*(length*length)/4 + B*length/2 + C,
uc = A*(length*length) + B*length + C.
Т.о. C = ua, а для A и B имеем систему уравнений:
A*(length*length)/4 + B*length/2 = ub - ua,
A*(length*length) + B*length = uc - ua.
Умножим первое уравнение на четыре, вычтем из него второе:
4*A*(length*length)/4 + 4*B*length/2 -
A*(length*length - B*length = 4*(ub - ua) - (uc - ua),
B*length = 4*(ub - ua) - (uc - ua),
B = (4*(ub - ua) - (uc - ua)) / length.
Умножим первое уравнение на два, вычтем его из второго:
A*(length*length) + B*length -
2*A*(length*length)/4 - 2*B*length/2 = (uc - ua) - 2*(ub - ua),
A*(length*length)/2 = (uc - ua) - 2*(ub - ua),
A = (2*(uc - ua) - 4*(ub - ua)) / (length*length).
Получили формулы для A, B, C. Найдем теперь du и ddu. Для текущей точки x имеем:
du(x) = u(x+1) - u(x),
du = (A*(x+1)*(x+1)+B*(x+1)+C) - (A*x*x+B*x+C) =
= A*(2*x+1) + B,
ddu(x) = du(x+1) - du(x),
ddu = (A*(2*(x+1)+1)+B) - (A*(2*x+1)+B) = 2*A.
Т.о., начальные значения u, du, ddu будут равны
u = C,
du = A + B,
ddu = 2*A.
А по известным начальным значениям u, du, ddu последовательно вычисляем значения u в любой точке:
u(0), du(0), ddu - известны
u(1) = u(0) + du(0), du(1) = du(0) + ddu
u(2) = u(1) + du(1), du(2) = du(1) + ddu
u(3) = u(2) + du(2), du(3) = du(2) + ddu
...
Для v все делается полностью аналогично.
Таким образом, рисование строки будет выглядеть примерно так:
// ...
// считаем u, v для начала, середины и конца строки
ua = uz_start / z1_start;
va = vz_start / z1_start;
ub = (uz_start + uz_end) / (z1_start + z1_end);
vb = (vz_start + vz_end) / (z1_start + z1_end);
uc = uz_end / z1_end;
vc = vz_end / z1_end;
// считаем начальное du и ddu
pa = 2 * ((uc - ua) - 2 * (ub - ua)) / (length * length);
pb = (4 * (ub - ua) - (uc - ua)) / length;
pc = ua;
u = pc;
du = pa + pb;
ddu = 2 * pa;
// считаем начальное dv и ddv
pa = 2 * ((vc - va) - 2 * (vb - va)) / (length * length);
pb = (4 * (vb - va) - (vc - va)) / length;
pc = v_a;
v = pc;
dv = pa + pb;
ddv = 2 * pa;
// рисуем кусок
while (length--) {
putpixel(current_sx, current_sy, texture[v][u]);
u += du;
v += dv;
du += ddu;
dv += ddv;
}
// ...
По сравнению с перспективно-корректным текстурированием имеем более медленный внутренний цикл, но меньшее для длинных строк количество делений. Расчет ua, va и иже с ними можно сделать с помощью трех делений, деления на length и (length*length) можно заменить умножениями на 1/length и 1/(length*length), беря эти значения из заранее посчитанной таблички. Т.о., на строку приходится три деления независимо от ее длины. Качество более-менее приемлемое; а для коротких строк можно использовать обычную линейную интерполяцию, точно так же, как и в случае с перспективно-корректным текстурированием. Получаем вполне конкурентоспособный метод текстурирования.
