Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая |
Лишь сравнительно недавно стали появляться первые работы, посвященные рендерингу покрытым мехом поверхностей в реальном времени. Основной причиной такой задержки, по-видимому, является то обстоятельство, что мех представляет собой скорее объем (т.е. некоторую область пространства), чем поверхность.
Хотя мех и можно представить себе при помощи большого количества ломаных (по одной для каждой ворсинки), в большинстве случаев это оказывается просто неприемлемым из-за очень больших вычислительных затрат на обработку каждой ворсинки (хотя на сайте компании NVIDIA можно найти описание демки Nalu, использующей ломаные для моделирования волос русалки).
Гораздо более распространенным (и менее ресурсоемким) является использование объемного представления меха, т.е. задание меха при помощи трехмерных текстур. Тогда значение произвольного тексела (точнее воксела) такой текстуры определяет проходит ли какая-либо ворсинка через соответствующую точку пространства.
Довольно часто, чтобы не использовать текстуры с большим разрешением (что требует очень большого объема памяти) используют текстуру меньшего размера, где в текселе задается как-бы суммарная степень прохождения волосков через соответствующую область пространства.
Впервые еще Kajya и Kay использовали подобное представление для рендеринга меха методом трассировки лучей. Данный поход дает изображения очень высокого качества и довольно прост в реализации (достаточно просто отслеживать переход луча из воксела в воксела до попадания в непустой воксел), но вряд ли пригоден для рендеринга в реальном времени (по крайней мере для современных персональных компьютеров).
Сейчас для рендеринга меха (точнее, объектов, покрытых мехом) обычно используется следующий подход - трехмерная текстура, задающая мех, рассматривается как набор (обычно 16-32) двухмерных текстур, соответствующих пересечению меха плоскостью, параллельной поверхности.
Тогда, если мы хотим нарисовать сферу, покрытую мехом, и мы располагаем соответствующим набором двухмерных текстур, то можно после вывода сферы, вывести набор сфер, описанных вокруг исходной.
При этом радиуса каждой следующей больше радиуса предыдущей на некоторую небольшую величину и на эту сферу натягивается очередная текстура из набора (см. рис. 1).
Рис 1. Семейство сфер.
При этом все эти сферы выводятся начиная с самой внутренней и заканчивая самой внешней и с режимом наложения (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA).
На следующем рисунке приводится получающееся при этом изображения (правда не сферы, а тора, поскольку наложение прямоугольной текстуры на сферу сопровождается появлением особенностей).
Рис 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 );
Вместо набора двухмерный текстур можно использовать всего одну текстуру, но тогда получается "слишком аккуратный" мех, выглядящий очень искусственно (см следующий рисунок).
Рис 3. "Волосатый" тор, полученный при использовании всего одной текстуры.
Рис 4. Текстура, используемая для построения изображения 3.
Обратите внимание, что для каждого слоя задается свое значение прозрачности, изменяющееся от shadowMin до shadowMax в зависимости от номера слоя.
Если в качестве режима наложения использовать (GL_SRC_ALPHA, GL_ONE), то мы получим "светящийся" мех, изображенный на следующем рисунке.
Рис 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) близко к нулю).
Рис 7. Построение fins.
Рис 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.