faqs.org.ru

 Главная > Программирование > Программирование графики >

demo.design 3D programming FAQ

Секция 3 из 6 - Предыдущая - Следующая
Все секции - 1 - 2 - 3 - 4 - 5 - 6

    length = x_end - x_start;

    // расчет u/Z, v/Z, 1/Z, u, v в начале самого первого куска
    uZ_a = uZ_start;
    vZ_a = vZ_start;
    Z1_a = Z1_start; // это 1/Z
    u_a = uZ_a / Z1_a;
    v_a = vZ_a / Z1_a;

    // рисуем куски по 16 пикселов
    while (length >= 16) {
      uZ_b = uZ_a + 16 * duZ_dsx; // расчет u/Z, v/Z, 1/Z, u, v в конце куска
      vZ_b = vZ_a + 16 * dvZ_dsx;
      Z1_b = Z1_a + 16 * dZ1_dsx;
      u_b = uZ_b / Z1_b;
      v_b = vZ_b / Z1_b;

      u = u_a; // начинаем текстурирование с начала куска
      v = v_a;
      du = (u_b - u_a) / 16; // можно сделать >> 4, используя fixedpoint
      dv = (v_b - v_a) / 16; // можно сделать >> 4, используя fixedpoint

      // рисуем 16 пикселов старым добрым "аффинным" методом
      len = 16;
      while (len--) {
        putpixel(current_sx, current_sy, texture[(int)v][(int)u]);
        u += du;
        v += dv;
        current_sx++;
      }
      length -= 16;

      // конец куска становится началом следующего куска
      uZ_a = uZ_b;
      vZ_a = vZ_b;
      Z1_a = Z1_b;
      u_a = u_b;
      v_a = v_b;
    }

    // дорисовываем "хвост" линии, если он непуст
    if (length != 0) {
      uZ_b = uZ_a + length * duZ_dsx;
      vZ_b = vZ_a + length * dvZ_dsx;
      Z1_b = Z1_a + length * dZ1_dsx;
      u_b = uZ_b / Z1_b;
      v_b = vZ_b / Z1_b;

      u = u_a; // начинаем текстурирование с начала куска
      v = v_a;
      du = (u_b - u_a) / length;
      dv = (v_b - v_a) / length;

      // рисуем остаток пикселов старым добрым "аффинным" методом
      while (length--) {
        putpixel(current_sx, current_sy, texture[v][u]);
        u += du;
        v += dv;
        current_sx++;
      }
    }
    // ...

Как и в 4.2, пройдемся подобным куском кода по всем строкам грани, не забыв
вместо "// ..." вставить интерполяцию всяких там [u/v/1]Z_start, содранную
с интерполяции u_start.. и - о чудо, текстурированная с учетом перспективы
грань!

Осталось сказать еще пару слов о кое-какой оптимизации.

Во-первых, два деления при расчете u и v в цикле прорисовки можно (и нужно)
заменить на одно - посчитать tmp = 1/Z, дальше u = uZ * tmp, v = vZ * tmp.

Во-вторых, немного поменяв местами блоки расчета очередной пары точных
значений u и v и прорисовки очередного куска линии, можно добиться того,
что это самое одно деление, нужное для расчета u и v для *следующего* куска
будет находиться сразу перед прорисовкой *текущего* куска. А в этом случае
деление может исполняться в сопроцессоре одновременно с отрисовкой куска
линии в процессоре. То есть единственная медленная операция будет считаться
на полную халяву! Получим перспективно-корректное текстурирование, которое
(теоретически) будет работать ненамного медленнее аффинного.

В-третьих, деление на length при дорисовке хвостика длиной от 1 до 15
пикселов можно заменить на умножение на 1/length, заранее посчитав табличку
для значений 1/length.

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

    x_start = A.sx+(B.sy-A.sy)*(C.sx-A.sx)/(C.sy-A.sy),
    x_end = B.sx,
    longest_length = x_end - x_start,

все равно мы ее считаем для расчета du_dsx или duZ_dsx и иже с ними.

4.4. Параболическое
-------------------

Этот метод основан на приближении 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),
беря эти значения из заранее посчитанной таблички. Т.о., на строку приходится
три деления независимо от ее длины. Качество более-менее приемлемое; а для
коротких строк можно использовать обычную линейную интерполяцию, точно так же,
как и в случае с перспективно-корректным текстурированием. Получаем вполне
конкурентоспособный метод текстурирования.

4.5. Билинейная фильтрация текстур
----------------------------------

Да-да, это именно тот метод, с помощью которого смазывают текстуры всякие
ускорители типа 3Dfx. Итак, пусть у нас есть какая-то текстура. Текстура у
нас - это 2D картинка, а 2D картинка в свою очередь - набор замеров цвета
через какие-то промежутки. В реальной же жизни цвет не меняется скачком
через каждый, например, миллиметр, а является какой-то непрерывной функцией
от положения, причем меняется довольно плавно. При обычном текстурировании
мы получаем координаты в текстуре, округляем их до ближайшего целого числа
и выбираем нужный цвет из текстуры. То есть мы как бы берем значение цвета
в самой близкой к рисуемой точке сетки замеров цвета, поэтому у нас цвет
резко меняется, оставаясь непрерывным между узлами сетки, поэтому возникает
эффект больших квадратов.

При билинейной фильтрации цвет всего-навсего линейно интерполируется между
узлами сетки замеров. То есть. Пусть в текущей точке у нас получились
координаты текстуры u, v - какие-то нецелые, вообще говоря, числа. Тогда
по целым частям u, v определяется, между какими узлами сетки (если угодно,
между какими пикселами текстуры) находится наша точка, а по дробным - как
близко она находится к каждому из узлов. Вот картинка.

    1------------2
    |            |
    a    *       b
    |            |
    |            |
    |            |
    3------------4

Здесь 1, 2, 3, 4 - "окружающие" точку пикселы текстуры (они же узлы сетки
замера цвета). Пусть iu, iv - целые части координат текстуры точки u, v; fu,
fv - дробные части. Тогда 1, 2, 3, 4 имеют координаты в текстуре (iu,iv),
(iu+1,iv), (iu,iv+1), (iu+1,iv+1). Проинтерполируем какую-то компоненту
цвета (R, G или B) по прямым 1-3 и 2-4:

    a.c = 1.c + (3.c - 1.c) * fv,
    b.c = 2.c + (4.c - 2.c) * fv,

то есть

    a.c = c[iu][iv] + (c[iu][iv+1] - c[iu][iv]) * fv,
    b.c = c[iu+1][iv] + (c[iu+1][iv+1] - c[iu+1][iv]) * fv,

Теперь проинтерполируем цвет по прямой ab в нашей точке:

    c = a.c + b.c * fu,

Проинтерполировав по этой формуле каждую компоненту цвета, получим наконец
готовый результат - цвет точки, но уже с учетом билинейной фильтрации.

Здесь у нас получилось по три умножения на компоненту. То есть в сумме девять
умножений на пиксел. Можно, конечно, честно считать по этим формулам, делая
девять умножений для каждого пиксела. Но можно заменить все умножения на
выборки по таблице. u, v обычно - это fixedpoint; fu, fv - тоже (кстати, в
случае с fixedpoint целые и дробные части вычисляются ровно одним and'ом).
Пусть мы используем 24-битный цвет и 16:16 fixedpoint; тогда одна компонента
цвета занимает 8 бит, а дробную часть можно одним сдвигом перевести в 24:8
fixedpoint. Получаем 256 возможных значений для любой компоненты цвета и 256
возможных значений для дробной части, то есть - табличка 256x256. Если цвет
15/16-битный, или используется более грубое (скажем, до пяти бит) округление
дробной части, то табличка становится еще меньше. Памяти, конечно, не жалко,
но кэш-память пока не резиновая, так что чем меньше lookup-таблица, тем оно
лучше для скорости. Вот и все.

Осталось только упомянуть, что лучше занести в табличку не байты, а слова,
для данного примера это будет 8:8 fixedpoint, и складывать все результаты
тоже как слова, а потом сдвигом переводить обратно в целые числа. Иначе
(особенно в случае 15/16-битных режимов) будет заметен небольшой шум на
текстуре, появляющийся из-за ошибок округления.

4.6. Мипмэппинг
---------------

Если полигон относительно сильно удален или повернут, так, что соседним
пикселам на экране соотвествуют сильно разнесенные точки текстуры, то
возникают всякие неприятные артефакты - можно считать, что потому, что при
текстурировании мы выбираем лишь какую-то одну точку текстуры, а реально в
экранный пиксел будет проецироваться несколько текселов (точек текстуры).
Вообще идеальным методом было бы следующее: провести до пересечения с гранью
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(). Вот,
в общем-то, и все.

5. Освещение
============

5.1. Модель освещения
---------------------

Освещенность произвольно взятой точки P, появившуюся из-за источника света,
излучающего во все стороны (omnilight) в общем случае будем вычислять по
уравнению Фонга:

    ambient = Ka,
    diffuse = Kd * cos(N, L),
    specular = Ks * pow(cos(R, V), Ns),
    intensity = ambient + amp * (diffuse + specular).

                 N
                 |
           L     |     R          V
            \    |    /        /
             \   |   /      /
              \  |  /    /
               \ | /  /
                \|//
    -------------P--------------

Здесь использованы следующие обозначения:
    Ka - коэффициент фоновой интенсивности (характеристика окружающей среды)
    Kd - коэффициент рассеяния (характеристика поверхности)
    Ks - коэффициент отражения (характеристика поверхности)
    Ns - коэффициент вида отражения (характеристика поверхности)
    amp - "мощность" источника света
    P - рассматриваемая точка
    N - нормаль к поверхности изображаемого объекта в точке P
    L - вектор, проведенный из точки P в источника света (луч света)
    V - вектор, проведенный из точки P в "точку зрения" камеры
    R - отраженный луч света (отражение L относительно N)
    ambient - "фоновая" освещенность
    diffuse - "рассеянная" освещенность
    specular - "отраженная" освещенность
    intensity - освещенность (суммарная)
    cos(A,B) - косинус угла между векторами A и B

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

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

Да, косинус угла между векторами считается как

    cos(A,B) = A*B/(|A|*|B|),

где A*B - скалярное произведение векторов, |A| и |B| - их длины. Поэтому
имеет смысл все векторы перед использованием привести к длине 1, тогда
косинус угла между векторами будет равен их скалярному произведению.
Небольшая оптимизация.

5.2. Расчет нормали к объекту
-----------------------------

Во всех формулах для освещенности у нас так или иначе будет фигурировать
вектор N - нормаль к объекту в точке P. Сразу возникает вопрос, а как же
этот вектор считать.

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

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

Для вящей понятности приведу кусок кода:

    // ...
    for (i = 0; i < numberOfVertics; i++) {
      vertexNormal[i].x = 0;
      vertexNormal[i].y = 0;
      vertexNormal[i].z = 0;
    }
    for (i = 0; i < numberOfVertics; i++) {
      for (j = 0; j < numberOfFaces; j++) {
        if (face[j].vertex0 == i ||
            face[j].vertex1 == i ||
            face[j].vertex2 == i)
        {
          vertexNormal[i].x += faceNormal[j].x;
          vertexNormal[i].y += faceNormal[j].y;
          vertexNormal[i].z += faceNormal[j].z;
        }
      }
    }
    // ...

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

    // ...
    for (i = 0; i < numberOfVertics; i++) {
      vertexNormal[i].x = 0;
      vertexNormal[i].y = 0;
      vertexNormal[i].z = 0;
    }
    for (i = 0; i < numberOfFaces; i++) {
      vertexNormal[face[i].vertex0].x += faceNormal[j].x;
      vertexNormal[face[i].vertex1].y += faceNormal[j].y;
      vertexNormal[face[i].vertex2].z += faceNormal[j].z;
    }
    // ...

5.3. Освещение по Ламберту
--------------------------

Это, видимо, самое сильное упрощение формулы, которое можно придумать.
Делаются такие предположения:
    - V не сильно зависит от P, т.о. V принимается постоянным для всей грани
    - L не сильно зависит от P, т.о. L принимается постоянным для всей грани
    - Ks = 0 (то есть грань не отражает свет, а только рассеивает)
    - нормаль к объекту N равна нормали к грани n в любой точке грани P

В этом случае формула принимает вид

    intensity = ambient + amp * cos(n, L),

где n - нормаль к грани.

Причем в этой формуле, по предположениям, нет величин, зависящих от P. Так
что освещенность равна константе для всей грани, то есть все точки грани
освещены одинаково. Тогда достаточно посчитать ее лишь один раз на грань.
Осталось упомянуть, что освещение по Ламберту обычно принято называть flat
shading.

5.4. Освещение по Гуро
----------------------

Здесь делаются такие предположения:
    - Ks = 0 (то есть грань не отражает свет, а только рассеивает)
    - освещенность меняется по грани линейно

Ну, а раз освещенность меняется линейно - так же, как и координаты текстуры
u, v - можно использовать точно такую же процедуру, как для старого доброго
текстурирования, только вместо двух координат текстуры u, v нам надо будет
интерполировать одну величину - освещенность. В вершинах она считается по
той же самой формуле (с учетом Ks = 0, конечно),

    intensity = ambient + amp * cos(N, L).

Здесь за N в вершине берется как раз нормаль к объекту в вершине (vertex
normal), посчитанная так, как описано в 5.2.

То есть, для написания процедуры освещения по Гуро достаточно написать три
строчки расчета освещенности (обозначать ее будем c) в вершинах грани по
приведенной здесь формуле и заменить в своей процедуре текстурирования u на
c, выкинув при этом v (оно нам не нужно, нас интересует только c). Чтобы
добавить освещение по Гуро в текстурирование, надо, соответственно, добавить
полностью скопированный с расчета u расчет c. Что делать с полученным по
цвету найденного через (u, v) в текстуре пиксела и освещенности с, описано в
соответствующем пункте.

Заметим, что освещение по Гуро - это не линейная интерполяция освещенности
по изображению грани на экране, как это обычно принято и понимать и делать.
Освещенность меняется линейно по трехмерной грани, а вовсе не по ее проекции.
То есть, вообще говоря, здесь мы тоже должны учитывать перспективу. Правда,
линии одинаковой освещенности все равно будут прямыми, но выглядеть картинка,
освещенная по Гуро с учетом перспективы, будет все же покорректнее.

5.5. Освещение по Фонгу
-----------------------

Здесь принято делать, как минимум, такие предположения:
    - Ks = 0 (то есть грань не отражает свет, а только рассеивает)

Да-да, здесь нет никакой ошибки. Практически все обычно используемые (в demo
по меньшей мере) методы т.н. "освещения по Фонгу" НЕ учитывают отраженной
компоненты освещенности.

Здесь будет рассказано о самом, наверное, популярном методе освещения по
Фонгу, который сводит освещение к текстурированию по определенной текстуре.

Этот метод базируется на таких добавочных предположениях:
    - L - константа (как бы точечный источник, удаленный бесконечно далеко)
    - длина единичной нормали к объекту при интерполяции между вершинами
      грани НЕ меняется

То есть, утверждается, что если в вершинах нормаль к объекту имеет длину 1,
то при интерполяции этой нормали между вершинами по какой-то грани мы будем
получать в каждой точке нормаль той же самой длины 1. На самом деле это вовсе
даже не так, но для несильно разнящихся углов наклона нормалей приблизительно
верно.

Так как Ks = 0, а длина N по предположению равна 1 на всей грани, имеем:

    intensity = ambient + amp * (N * L).

Рассмотрим упрощенный случай, когда вектор L = (0,0,1). Общий случай можно
без особых вычислительных затрат привести к этому упрощенному, как - будет
рассказано чуть позже. Так вот, в этом случае

    intensity = ambient + amp * (N.x * L.x + N.y * L.y + N.z * L.z) =
              = ambient + amp * (N.x * 0 + N.y * 0 + N.z * 1) =
              = ambient + amp * N.z =
              = ambient + amp * sqrt(1 - (N.x * N.x + N.y * N.y)).

То есть интенсивность выражается через N.x, N.y, а эти величины меняются
линейно. N.x и N.y у нас - числа с плавающей запятой от -1 до 1 (т.к. длина
вектора равна 1), интерполировать их - занятие медленное, да корень считать
раз в пиксел тоже не хочется. Поэтому вместо интерполяции N.x и N.y обычно
интерполируют, например, 128*(N.x+1) и 128*(N.y+1), причем уже в целых
числах. Тогда все возможные значения таким образом отмасштабировнных N.x,
N.y - это 0, 1, ..., 255. Поэтому можно заранее посчитать табличку значений
intensity для каждой пары отмасштабировнных N.x, N.y.

То есть, мы линейно интерполируем 128*(N.x+1) и 128*(N.y+1) (эти значения
меняются тоже линейно, раз N.x, N.y меняются линейно) и по ним по таблице
определяем интенсивность. Это и есть текстурирование, только в качестве
текстуры используется таблица освещенности размером 256x256 (или любым
другим), а в качестве координат текстуры u, v для каждой вершины берутся
отмасшатбированные координаты нормали в этой вершине.

Таблица, согласно всего вышеупомянутого, считается так:

    // ...
    for (i = 0; i < 256; i++) {
      for (j = 0; j < 256; j++) {
        r =
          pow((i - 128) / 256.0, 2) +  // это N.x*N.x
          pow((j - 128) / 256.0, 2);   // это N.y*N.y
        if (r > 1) r = 1; // длина N меньше 1, поэтому r > 1 быть не может
        phongTable[i][j] = amp * sqrt(1 - r);
      }
    }
    // ...

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

    // ...
    for (i = 0; i < 256; i++)
      for (j = 0; j < 256; j++)
        phongTable[i][j] =
          amp * pow(sin(i * PI / 256) * sin(j * PI / 256), 4);
    // ...

Для полного комплекта осталось только привести кусочек кода по вычислению
координат в этой таблице:

    // ...
    len = N.x * N.x + N.y * N.y + N.z * N.z;
    N.x /= len; // на случай, если длина N не равна 1
    N.y /= len;
    N.z /= len;
    u = (1 + N.x) * 128; // собственно расчет координат
    v = (1 + N.y) * 128;
    // ...

Теперь вернемся к вопросу о том, как привести случай с произвольным вектором
освещения к только что рассмотренному, где L = (0,0,1). Здесь все вроде бы
просто. Просто применим к нормалям в вершинах любой поворот, совмещающий наш
произвольный вектор света с вектором (0,0,1). Скалярное произведение при этом
не изменяется, поэтому так делать можно. Ну, а после этого поворота уже имеем
только что расписанный упрощенный случай.

Этот поворот нормалей в каждой вершине не требует практически никаких затрат
по следующей причине. Поворот сам по себе, конечно, достаточно медленная
процедура и процессорное время отъедает. Но при движении и вращении камеры
и объекта мы все равно должны будем соответственно поворачивать нормали. Так
вот, эти два поворота можно совместить в один. Если использовать матрицы,
все это делается совсем просто - достаточно перемножить (в нужном порядке!)
матрицу собственного поворота объекта, матрицу перехода от произвольной
камеры к нашей "стандартной" камере и матрицу перехода от произвольного
вектора света к "стандартному" вектору света (0,0,1). Т.е. добавится расчет
этой матрицы перехода и одно матричное умножение на объект, а это уже мелочь.

5.6. Как совместить текстуру и освещение
----------------------------------------

Прежде всего, немного теории. Нам понадобится знать то, что конечный цвет
пиксела с составляющими (r,g,b) и освещенного цветом (light_r,light_g,light_b)
считается как

     result_r = r * light_r / max(light_r);
     result_g = g * light_g / max(light_g);
     result_b = b * light_b / max(light_b);

Здесь max(light_r) - это максимально возможное значение для light_r. Не
максимальное по всем тем градациям освещенности, что мы используем, а вообще
максимальное для всех возможных градаций. Например, если у нас значения для
light_r могут гулять от 0 до 255, а текущий источник света имеет цвет (30,
40,50), то соответственно градации освещенности будут равны (k*30,k*40,k*50),
где 0 <= k <= 1, то max(light_r) = 255, а не 30. Немного путаное объяснение,
но вообще это известная и понятная почти всем вещь.

5.6.1. 256-цветные режимы
~~~~~~~~~~~~~~~~~~~~~~~~~
Метод 1: заранее посчитать таблицу, переводящую пару (цвет, освещенность) в
цвет. Можно не менять палитру, а искать для каждой пары (цвет, освещенность)
наилучшим образом приближающий ее цвет; а можно (как описано в demo.design
FAQ) взять палитру и сгенерировать из нее true color картинку 256x256, в
которой пиксел (x, y) нарисован цветом (уже true color!), соответствующим
цвету x и освещенности y, а потом перевести ее чем-нибудь типа Image Alchemy
в 256 цветов. После чего использовать палитру получившейся картинки, а в
качестве таблицы (colorTable) будет сама получившаяся картинка:

    outputColor = colorTable[intensity][color].

Метод 2: если нас устроит использовать немного цветов и градаций освещения,
то тогда в палитру можно впихнуть все возможные градации всех используемых
цветов. Тогда определение нужного индекса в палитре - это одно умножение,
а лучше - сдвиг, и одно сложение. Пример: пусть у нас есть 8 цветов и 32

Секция 3 из 6 - Предыдущая - Следующая

Вернуться в раздел "Программирование графики" - Обсудить эту статью на Форуме
Главная - Поиск по сайту - О проекте - Форум - Обратная связь

© faqs.org.ru