Рендеринг меха

В реальной жизни довольно часто встречаются объекты, покрытые мехом (хотя бы те же импы из первого Дума были покрыты мехом). Однако рендеринг покрытых мехом поверхностей длительное время представлял собой достаточно сложную задачу.

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

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

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

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

Впервые еще Kajya и Kay использовали подобное представление для рендеринга меха методом трассировки лучей. Данный поход дает изображения очень высокого качества и довольно прост в реализации (достаточно просто отслеживать переход луча из воксела в воксела до попадания в непустой воксел), но вряд ли пригоден для рендеринга в реальном времени (по крайней мере для современных персональных компьютеров).

Сейчас для рендеринга меха (точнее, объектов, покрытых мехом) обычно используется следующий подход - трехмерная текстура, задающая мех, рассматривается как набор (обычно 16-32) двухмерных текстур, соответствующих пересечению меха плоскостью, параллельной поверхности.

Тогда, если мы хотим нарисовать сферу, покрытую мехом, и мы располагаем соответствующим набором двухмерных текстур, то можно после вывода сферы, вывести набор сфер, описанных вокруг исходной.

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

fur as a set of spheres

Рис 1. Семейство сфер.

При этом все эти сферы выводятся начиная с самой внутренней и заканчивая самой внешней и с режимом наложения (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA).

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

Fur torus

Рис 2. "Волосатый" тор.

Ниже приводится фрагмент программы (ссылка на полный текст в конце статьи), выводящей данные слои.

                                // draw torus itself
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glEnable           ( GL_TEXTURE_2D );
    glBindTexture      ( GL_TEXTURE_2D, decalMap );

    glMatrixMode   ( GL_MODELVIEW );
    glPushMatrix   ();

    glRotatef    ( rot.x, 1, 0, 0 );
    glRotatef    ( rot.y, 0, 1, 0 );
    glRotatef    ( rot.z, 0, 0, 1 );

    glEnable   ( GL_CULL_FACE );
    glCullFace ( GL_BACK );

    torus.draw  ();

                                // now start drawing shells
    float  scale     = 0.5;
    float  shadowMin = 0.2;
    float  shadowMax = 0.5;

    glDisable      ( GL_CULL_FACE );
    glEnable       ( GL_BLEND );
    glBlendFunc    ( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
    glMatrixMode   ( GL_TEXTURE );
    glLoadIdentity ();
    glScalef       ( 5, 10, 5 );
    glMatrixMode   ( GL_MODELVIEW );

                                // draw shells from innermost to outermost
    for ( int i = 0; i < 16; i++ )
    {
        float  t      = (float) i / 16.0;
        float  shadow = shadowMin * (1 - t) + shadowMax * t;

        glBindTexture ( GL_TEXTURE_2D, furTex [i] );
        glColor4f     ( 1, 1, 1, shadow );

        torus.draw ( t * scale );
    }

    glDisable ( GL_BLEND );

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

fur torus with only one fur texture

Рис 3. "Волосатый" тор, полученный при использовании всего одной текстуры.

Fur texture for innermost level

Рис 4. Текстура, используемая для построения изображения 3.

Обратите внимание, что для каждого слоя задается свое значение прозрачности, изменяющееся от shadowMin до shadowMax в зависимости от номера слоя.

Если в качестве режима наложения использовать (GL_SRC_ALPHA, GL_ONE), то мы получим "светящийся" мех, изображенный на следующем рисунке.

glowing fur

Рис 5. Светящийся мех.

Если изменять текстурные координаты для выводимых слоев (их называют shells), то оказывается возможным управлять толщиной ворсинок, их наклоном (за счет этого можно сделать "причесанный" мех) и даже анимировать мех, заставляя его двигаться.

Проще всего осуществлять построение отдельных слоев (и расчет освещенности для них) при помощи вершинной программы. Для этого с каждой вершиной покрытой мехом поверхности свяжем вектор hairDir, направление которого задает примерное направление ворсинок, и скаляр hairLen задающий их длину (т.е. толщину слоя меха).

struct	Vertex
{
    Vector3D    pos;                        // position of vertex
    Vector2D    tex;                        // texture coordinates
    Vector3D    n;                          // unit normal
    Vector3D    t, b;                       // tangent and binormal
    Vector3D    hairDir;                    // hair direction (m.b. not normalized)
    float       hairLen;                    // hair length
};

Тогда вершинная программа для получения вершины очередного слоя просто сдвигает текущую вершину на величину hairDir*i/(numLayers-1), где i задает номер текущего слоя, а numLayers - общее число слоев.

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

В простейшем случае в качестве этого касательного вектора t может выступать нормированный вектор hairDir. Тогда мы получаем по-вершинное освещение, в конце статьи будет рассмотрено и попиксельное.

Напомним формулу для анизотропной модели освещения:

Диффузная компонента освещенности:

Id = Kd*C*(1 - (t,l)2)1/2

Бликовая компонента:

Is = Ks*(1 - (t,h)2)p/2

Общая освещенность является суммой этих компонент.

Здесь через t, l и h обозначены касательный вектор, вектор на источник света и среднее направление между направлением на источник света и на наблюдателя.

Приводимая ниже вершинная программа осуществляет смещение вершин для очередного слоя и вычисление освещенности по приведенной выше анизотропной модели.

!!ARBvp1.0
#
# simple vertex shader for fur lighting
#
# on entry:
#      vertex.position
#      vertex.normal       - normal vector (n) of TBN basic
#      vertex.texcoord [0] - normal texture coordinates
#      vertex.texcoord [1] - tangent vector  (t)
#      vertex.texcoord [2] - binormal vector (b)
#	   vertex.texcoord [3] - (furDir,furLen)
#
#      program.local [0] - eye position
#      program.local [1] - light position
#      program.local [2] - shell offset (in x component)
#
#      state.matrix.program [0] - rotation matrix for the object
#
# on exit:
#     result.texcoord [0] - texture coordinates
#     result.texcoord [1] - l
#     result.texcoord [2] - h
#

# We assume that object whose vertices are passed are transformed by program [0] matrix
# Instead of transforming vertex and TBN basis with this matrix we transform light pos
# and eye pos with inverse matrix to leave TBN intact.
# This way we keep l, v and h vectors correct
#

ATTRIB  pos     = vertex.position;
ATTRIB  furAttr = vertex.texcoord [3];
PARAM   eye     = program.local [0];
PARAM   light   = program.local [1];
PARAM   step    = program.local [2];
PARAM   mvp [4] = { state.matrix.mvp };
PARAM   mv0 [4] = { state.matrix.program [0].inverse };  # inverse rotation matrix

PARAM   one         = 1.0;
PARAM   diffusePow  = 1.0;
PARAM   specularPow = 30.0;
PARAM   ka          = 0.2;
PARAM   specColor   = { 1, 1, 1, 1 };
PARAM   furTexScale = { 7, 12, 7 };

TEMP    l, v, h, temp;
TEMP    lt, ht;
TEMP    vt, et;
TEMP    t, color;

            # transform light position
DP4	lt.x, light, mv0 [0];
DP4	lt.y, light, mv0 [1];
DP4	lt.z, light, mv0 [2];
DP4	lt.w, light, mv0 [3];

            # transform eye position
DP4	et.x, eye, mv0 [0];
DP4	et.y, eye, mv0 [1];
DP4	et.z, eye, mv0 [2];
DP4	et.w, eye, mv0 [3];

            # compute l (vector to light)
ADD	l, -pos, lt;
DP3	temp.x, l, l;
RSQ	temp.x, temp.x;
MUL	l, l, temp.x;

            # compute v (vector to viewer)
ADD	v, -pos, et;
DP3	temp.x, v, v;
RSQ	temp.x, temp.x;
MUL	v, v, temp.x;


            # compute h = (l+v)/2 and normalize it
ADD	h, l, v;
DP3	temp.x, h, h;
RSQ	temp.x, temp.x;
MUL	h, h, temp.x;

            # compute t
DP3	temp.x, furAttr, furAttr;   # normalize fur dir
RSQ	temp.x, temp.x;
MUL	t, furAttr, temp.x;

                                # compute dot products
DP3	temp.x, t, l;
DP3	temp.y, t, h;
                                # turn them into 1 - square
MUL	temp.xy, temp, temp;
ADD	temp.xy, one, -temp;

                                # compute powers
POW	temp.x, temp.x, diffusePow.x;
POW	temp.y, temp.y, specularPow.x;

                                # compute overall lighting and store it
ADD	temp.x, temp.x, ka;         # add ambient lighting

MUL	result.color,             vertex.color, temp.x;
MUL	result.color.secondary,   specColor,    temp.y;

                                # copy initial transparency
MOV result.color.w,           vertex.color.w;
MOV result.color.secondary.w, vertex.color.w;

                                # store texcoord [0]
MUL	result.texcoord [0], vertex.texcoord [0], furTexScale;

                                # move point along t using step value
MUL	temp,   t, step.x;
ADD	temp,   temp, vertex.position;
MOV	temp.w, vertex.position.w;

                                # transform position into clip space
DP4	result.position.x, temp, mvp [0];
DP4	result.position.y, temp, mvp [1];
DP4	result.position.z, temp, mvp [2];
DP4	result.position.w, temp, mvp [3];

               # we're done
END

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

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

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

Это довольно легко реализовать, используя register combiner'ы.

                                                 // draw the light
    glMatrixMode ( GL_MODELVIEW );
    glPushMatrix ();

    glTranslatef       ( light.x, light.y, light.z );
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glDisable          ( GL_TEXTURE_2D );
    glColor4f          ( 1, 1, 1, 1 );
    glutSolidSphere    ( 0.1f, 15, 15 );
    glPopMatrix        ();

                                                // set matrix0
    glMatrixMode   ( GL_MATRIX0_ARB );
    glLoadIdentity ();
    glRotatef      ( rot.x, 1, 0, 0 );
    glRotatef      ( rot.y, 0, 1, 0 );
    glRotatef      ( rot.z, 0, 0, 1 );

                                                // draw torus itself
    glActiveTextureARB ( GL_TEXTURE0_ARB );
    glEnable           ( GL_TEXTURE_2D );
    glBindTexture      ( GL_TEXTURE_2D, decalMap );

    glMatrixMode   ( GL_MODELVIEW );
    glPushMatrix   ();

    glRotatef    ( rot.x, 1, 0, 0 );
    glRotatef    ( rot.y, 0, 1, 0 );
    glRotatef    ( rot.z, 0, 0, 1 );

    glEnable   ( GL_CULL_FACE );
    glCullFace ( GL_BACK );

    torus.draw  ();

                                               // now start drawing shells
    float   scale     = 0.5;
    float   shadowMin = 0.2;
    float   shadowMax = 0.5;

    glDisable      ( GL_CULL_FACE );
    glEnable       ( GL_BLEND );
    glBlendFunc    ( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

                                                // draw shells from innermost to outermost
    vertexProgram.enable   ();
    vertexProgram.bind     ();

    glEnable                ( GL_REGISTER_COMBINERS_NV );
    glCombinerParameteriNV  ( GL_NUM_GENERAL_COMBINERS_NV, 1 );


                                                // setup computing tex.a * primary-color.a
                                                // into spare0.a
                                                // A = tex0.a
    glCombinerInputNV ( GL_COMBINER0_NV, GL_ALPHA, GL_VARIABLE_A_NV, GL_TEXTURE0_ARB,
                        GL_UNSIGNED_IDENTITY_NV, GL_ALPHA );

                                                // B.a = primary-color.a
    glCombinerInputNV ( GL_COMBINER0_NV, GL_ALPHA, GL_VARIABLE_B_NV, GL_PRIMARY_COLOR_NV,
                        GL_UNSIGNED_IDENTITY_NV, GL_ALPHA );

    glCombinerOutputNV ( GL_COMBINER0_NV, GL_ALPHA,
                         GL_SPARE0_NV,          // AB
                         GL_DISCARD_NV,         // CD
                         GL_DISCARD_NV,         // AB+CD
                         GL_NONE,               // scale
                         GL_NONE,               // bias
                         GL_FALSE, GL_FALSE, GL_FALSE );

                                                // spare0.a contains tex.0*primary-color.a

                                                // A.rgb = primary-color
    glFinalCombinerInputNV ( GL_VARIABLE_A_NV, GL_PRIMARY_COLOR_NV, GL_UNSIGNED_IDENTITY_NV,
                             GL_RGB );

                                                // B.rgb = tex0.rgb
    glFinalCombinerInputNV ( GL_VARIABLE_B_NV, GL_TEXTURE0_ARB, GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // C = 0
    glFinalCombinerInputNV ( GL_VARIABLE_C_NV, GL_ZERO, GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // D = EF
    glFinalCombinerInputNV ( GL_VARIABLE_D_NV, GL_E_TIMES_F_NV, GL_UNSIGNED_IDENTITY_NV, GL_RGB );

                                                // E = secondary-color
    glFinalCombinerInputNV ( GL_VARIABLE_E_NV, GL_SECONDARY_COLOR_NV, GL_UNSIGNED_IDENTITY_NV,
                             GL_RGB );

                                                // F = tex0.a
    glFinalCombinerInputNV ( GL_VARIABLE_F_NV, GL_TEXTURE0_ARB, GL_UNSIGNED_IDENTITY_NV,
                             GL_ALPHA );

                                                // G.alpha = spare0.a
    glFinalCombinerInputNV ( GL_VARIABLE_G_NV, GL_SPARE0_NV, GL_UNSIGNED_IDENTITY_NV, GL_ALPHA );

    for ( int i = 0; i < 16; i++ )
    {
        float   t      = (float) i / 16.0;
        float   shadow = shadowMin * (1 - t) + shadowMax * t;

        glBindTexture   ( GL_TEXTURE_2D, furTex [i] );
        glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso );
        glColor4f       ( 1, 1, 1, shadow );

        vertexProgram.local [2] = Vector3D ( t * scale, 0, 0 );

        torus.draw ( 0 );
    }

    vertexProgram.disable ();

    glDisable ( GL_BLEND );
    glDisable ( GL_REGISTER_COMBINERS_NV );

Волосатый тор с вершинным освещением

Волосатый тор с вершинным освещением

Волосатый тор с вершинным освещением

Рис 6. Изображения "волосатого" тора с по-вершинным освещением.

Если же мы хотим использовать попиксельное освещение, то кроме набора текстур, задающих прохождения отдельных волосков, можно еще задать касательные вектора в этих точках.

Проще всего это также сделать при помощи дополнительного набора текстур.

Располагая текстурами, задающими как прохождение отдельных волосков, так и их направление, можно при помощи фрагментной программы осуществить вычисление освещенности для каждого фрагмента.

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

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

Ниже приводятся вершинная и фрагментная программы.

!!ARBvp1.0
#
# simple vertex shader for fur lighting
#
# on entry:
#      vertex.position
#      vertex.normal       - normal vector (n) of TBN basic
#      vertex.texcoord [0] - normal texture coordinates
#      vertex.texcoord [1] - tangent vector  (t)
#      vertex.texcoord [2] - binormal vector (b)
#      vertex.texcoord [3] - (furDir,furLen)
#
#      program.local [0] - eye position
#      program.local [1] - light position
#      program.local [2] - shell offset (in x component)
#
#      state.matrix.program [0] - rotation matrix for the object
#
# on exit:
#     result.texcoord [0] - texture coordinates
#     result.texcoord [1] - l
#     result.texcoord [2] - h
#     result.texcoord [3] - t
#

# We assume that object whose vertices are passed are transformed by program [0] matrix
# Instead of transforming vertex and TBN basis with this matrix we transform light pos
# and eye pos with inverse matrix to leave TBN intact.
# This way we keep l, v and h vectors correct
#

ATTRIB  pos     = vertex.position;
ATTRIB  furAttr = vertex.texcoord [3];
PARAM   eye     = program.local [0];
PARAM   light   = program.local [1];
PARAM   step    = program.local [2];
PARAM   mvp [4] = { state.matrix.mvp };
PARAM   mv0 [4] = { state.matrix.program [0].inverse };  # inverse rotation matrix
PARAM   half    = 0.5;
PARAM   furTexScale = { 7, 12, 7 };

TEMP    l, l2, v, v2, h, h2, t, temp;
TEMP    lt, ht, lt2;
TEMP    vt, et, tt;

            # transform light position
DP4	lt2.x, light, mv0 [0];
DP4	lt2.y, light, mv0 [1];
DP4	lt2.z, light, mv0 [2];
DP4	lt2.w, light, mv0 [3];

            # transform eye position
DP4	et.x, eye, mv0 [0];
DP4	et.y, eye, mv0 [1];
DP4	et.z, eye, mv0 [2];
DP4	et.w, eye, mv0 [3];

            # compute l (vector to light)
ADD	l, -pos, lt2;

            # transform it into tangent space
DP3	lt.x, l, vertex.texcoord [1];
DP3	lt.y, l, vertex.texcoord [2];
DP3	lt.z, l, vertex.normal;
MOV	lt.w, l.w;

            # store it into texcoord [1]
MOV	result.texcoord [1], lt;

            # compute v (vector to viewer)
ADD	v, -pos, et;

            # normalize it (we need to correctly compute h)
DP3	temp.x, v, v;
RSQ	temp.y, temp.x;
MUL	v, v, temp.y;

            # compute h = (l+v)/2 and normalize it
ADD	h, l, v;
DP3	temp.x, h, h;
RSQ	temp.x, temp.x;
MUL	h, h, temp.x;

            # transform it into tangent space
DP3	ht.x, h, vertex.texcoord [1];
DP3	ht.y, h, vertex.texcoord [2];
DP3	ht.z, h, vertex.normal;
MOV	ht.w, h.w;

            # store it into texcoord [2]
MOV	result.texcoord [2], ht;

            # compute t
DP3	temp.x, furAttr, furAttr;
RSQ	temp.x, temp.x;
MUL	t, furAttr, temp.x;

            # transform into tangent space and store it
DP3	result.texcoord [3].x, t, vertex.texcoord [1];
DP3	result.texcoord [3].y, t, vertex.texcoord [2];
DP3	result.texcoord [3].z, t, vertex.normal;
MOV	result.texcoord [3].w, 1;

            # store texcoord [0]
MUL	result.texcoord [0], furTexScale, vertex.texcoord [0];

            # copy primary and secondary colors
MOV	result.color,           vertex.color;
MOV	result.color.secondary, vertex.color.secondary;

            # move point along t using step value
MUL	temp,   t, step.x;
ADD	temp,   temp, vertex.position;
MOV	temp.w, vertex.position.w;

            # transform position into clip space
DP4	result.position.x, temp, mvp [0];
DP4	result.position.y, temp, mvp [1];
DP4	result.position.z, temp, mvp [2];
DP4	result.position.w, temp, mvp [3];

            # we're done
END

Фрагментная программа.

!!ARBfp1.0
#
# simple fur shader
# on entry:
#     fragment.texcoord [0] - texture coordinates
#     fragment.texcoord [1] - l in tangent space
#     fragment.texcoord [2] - h in tangent space
#     fragment.texcoord [3] - t in tangent space
#
#     texture [0] - current fur layer texture
#

ATTRIB  l           = fragment.texcoord [1];
ATTRIB  h           = fragment.texcoord [2];
ATTRIB  t           = fragment.texcoord [3];
PARAM   one         = 1.0;
PARAM   diffusePow  = 1.0;
PARAM   specularPow = 30.0;
PARAM   ka          = 0.2;
PARAM   specColor   = { 1, 1, 1, 1 };

TEMP    ln, hn, color, h2, l2, n2, t2, temp, diffuse, dist;
TEMP    atten, ctex, dots, color2;

            # normalize l
DP3     dist.w, l, l;
RSQ     l2.w, dist.w;
MUL     l2.xyz, l, l2.w;

            # normalize h
DP3     h2.w, h, h;
RSQ     h2.w, h2.w;
MUL     h2.xyz, h, h2.w;

            # normalize t
DP3     t2.w, t, t;
RSQ     t2.w, t2.w;
MUL     t2.xyz, t, t2.w;

            # get value from fur texture
TEX     ctex, fragment.texcoord [0], texture [0], 2D;
MUL     ctex, ctex, fragment.color;     # modulate with current color (and transparency)

            # compute ((t,l), (t,h))
DP3     dots.x, l2, t2;
DP3     dots.y, h2, t2;
MUL     dots, dots, dots;               # square them
ADD     dots, 1, -dots;

            # compute powers
POW     dots.x, dots.x, diffusePow.x;
POW     dots.y, dots.y, specularPow.x;


                                        # now combine diffuse and specular lighting
MUL     color.rgb,  specColor, dots.y;  # specular component
MUL     color,      color,     ctex.a;
MUL     color2.rgb, ctex,      dots.x;  # diffuse component
ADD_SAT result.color, color, color2;
MOV     result.color.a, ctex.a;
END

Рис 7. "Волосатый тор" с попиксельным освещением.

Для улучшения видимости волосков на краю объектов (точнее на его контурной линии - месте, где нормаль ортогональна направлению на наблюдателя) иногда используется следующий прием: на ребрах, близких к контурной линии выводятся грани, перпендикулярные самой поверхности.

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

fins

Рис 7. Построение fins.

fins texture

Рис 8. Пример текстуры для fins.

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

Построение текстур для моделирования меха

Стандартным приемом для построения всех необходимых текстур для рендеринга меха является моделирование волосков при помощи системы частиц.

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

Для этих частиц отслеживается пересечение ими набора плоскостей и в каждой точке пересечения в соответствующие текстуры заносятся цвет и направление частицы.

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

Для текстур, содержащих значения касательных, обычно используется специальная процедура "размытия", заносящая в пустые текселы значения касательной из ближайшего точки и слегка сглаживающая значения касательных.

Все эти процедуры были реализованы на языке Python, для работы с текстурами использовалась библиотека PIL.

Ниже приводится только один из этих скриптов - для создания набора исходных текстур высокого разрешения.

#
# script to build a set of fur textures and normal maps
#

import PIL, Image
import math, random

size         = 256                 # size of initial texture (before resizing)
density      = 0.55
curliness    = 1.0
eps          = 0.3
numLayers    = 16
tangentScale = 0.3
numParticles = round ( density * size * size )
binStep      = 16
numBins1     = size / binStep
numBins2     = numBins1 * numBins1
bins         = []

for i in range ( numBins2 ):
    bins.append ( [] )

class Particle:
    def __init__ ( self ):
        self.x      = random.random () * ( size - 1 );
        self.y      = random.random () * ( size - 1 );
        self.length = 1     #0.8 + 0.2 * random.random ()
        self.a1     = random.random () * math.pi * 4
        self.a2     = random.random () * math.pi * 4
        self.xm1    = 2 * (random.random () - 0.5) * curliness
        self.ym1    = 2 * (random.random () - 0.5) * curliness
        self.xm2    = 2 * (random.random () - 0.5) * curliness
        self.ym2    = 2 * (random.random () - 0.5) * curliness

    def posAt ( self, time ):
        xNew = self.x + self.xm1 * math.sin ( self.a1 * time ) +
               self.xm2 * math.sin ( self.a2 * time )
        yNew = self.y + self.ym1 * math.sin ( self.a1 * time ) +
               self.ym2 * math.sin ( self.a2 * time )

        return (xNew, yNew)

    def tangentAt ( self, time ):
        dx  = self.xm1 * self.a1 * math.cos ( self.a1 * time ) +
              self.xm2 * self.a2 * math.cos ( self.a2 * time )
        dy  = -self.ym1 * self.a1 * math.sin ( self.a1 * time ) -
              self.ym2 * self.a2 * math.sin ( self.a2 * time )
        dx  = dx * tangentScale
        dy  = dy * tangentScale
        len = math.sqrt ( dx * dx + dy * dy + 1 )

        return ( dx / len, dy / len, 1 / len )

    def distanceTo ( self, other ):
        return abs ( self.x - other.x ) + abs ( self.y - other.y )

    def overlaps ( self, particles, count ):
        b = bins [self.getBin ()]

        for p in b:
            if self.distanceTo ( p ) < eps:
                return 1

        return 0

    def getBin ( self ):
        i = int ( self.x / binStep )
        j = int ( self.y / binStep )

        return i * numBins1 + j

im   = Image.new ( "RGBA", (size, size) )               # colors and alpha map
im2  = Image.new ( "RGB",  (size, size) )               # tangent map
im3  = Image.new ( "RGB",  (size, size) )               # for filtered tangent map

            # init to (black,0)
def clearImage ( image ):
    for x in range ( size ):
        for y in range ( size ):
            image.putpixel ( (x,y), (0, 0, 0, 0) )

            # do "blurring of tangent maps"
            # we use only non-black pixels (to create real map without holes)
def blurTangentMap ( map, out ):

            # for every texel calc average on non-empty pixels
    for x in range ( size ):
        for y in range ( size ):
            sum   = [0,0,0]
            count = 0

            for dx in range ( -1, 2 ):
                for dy in range ( -1, 2 ):
                    x1 = (x + dx + size) % size
                    y1 = (y + dy + size) % size
                    c  = map.getpixel ( (x1,y1) )

                    if c [0] != 0 or c [1] != 0 or c [2] != 0:
                        count   = count + 1
                        sum [0] = sum [0] + c [0]
                        sum [1] = sum [1] + c [1]
                        sum [2] = sum [2] + c [2]

                                        # now average them and map to [0,1]
            if count > 0:
                sum [0] = sum [0] / (count)
                sum [1] = sum [1] / (count)
                sum [2] = sum [2] / (count)

            out.putpixel ( (x,y), (round ( sum [0] ), round ( sum [1] ), round ( sum [2] ) ) )

                                         # now create a set of particles
print "Creating", numParticles, " particles"

particles = []

for i in range ( numParticles ):
    while 1:
        p = Particle ()
        b = p.getBin ()

        if not p.overlaps ( particles, i ):
            particles.append ( p )
            bins [b].append  ( p )
            break

                                           # start particle simulation
for layer in range ( numLayers ):
    t = float ( layer ) / float ( numLayers )

    clearImage ( im  )
    clearImage ( im2 )

    for i in range ( numParticles ):
        if particles [i].length < t:        # length check
            continue

        pos = particles [i].posAt ( t )     # get position at
        x   = round ( pos [0] ) % size      # do wrapping
        y   = round ( pos [1] ) % size
        im.putpixel ( (x, y ), (255, 170, 64, 255) )

        tang  = particles [i].tangentAt ( t )
        red   = 128 + round ( 127 * tang [0] )
        green = 128 + round ( 127 * tang [1] )
        blue  = 128 + round ( 127 * tang [2] )

        im2.putpixel ( (x, y), (red, green, blue) )

                                             # here we should do "normal blur" of tangent map
    blurTangentMap ( im2, im3 )
    blurTangentMap ( im3, im2 )

    fileName  = "fur-map-%02d.png"         % ( layer )
    fileName2 = "fur-tangent-map-%02d.png" % ( layer )

    im. save ( fileName, "png" )
    im2.save ( fileName2, "png" )

    print "Layer %d done" % (layer)

В данном скрипте параметр size задает начальное разрешение текстур, параметр density задает плотность меха, а curliness - степень искривленности ворсинок.

Еще один скрипт - resample.py - используется для сжатия (со сглаживанием) текстур.

Полностью все эти скрипты можно скачать вместе со всем исходным кодом для этой статьи.

По этой ссылке можно скачать весь исходный код к этой статье. Также доступны для скачивания откомпилированные версии для M$ Windows, Linux и Mac OS X.

Valid HTML 4.01 Transitional

Напиши мне
Используются технологии uCoz