8.4 Обзор методов имитации акварели Моделирование акварели средствами компьютерной графики - одна из интереснейших подзадач NPR. В настоящее время существует несколько подходов к моделированию акварельных красок: Размытие изображения и затемнение краев Простейший подход использует размытие изображения и затемнение краев (четких границ на изображении), но этот метод не дает реалистичного результата. Методы, использующие покрытие изображения последовательностью штрихов В зависимости от желаемого эффекта можно настраивать множество параметров штрихов - размер, ориентацию, степень прозрачности и т.д. Если проводить анализ фильтруемого изображения, можно использовать дополнительные возможности - ориентация штрихов перпендикулярно градиенту, <незалезание> за края. Такие методы успешно применяются для имитации импрессионизма, однако для акварели это не дает нужного эффекта изображения получаются не очень естественными. Используются в некоторых коммерческих программах. Метод визуализации поверхностей с эффектами акварели Очень любопытный подход, использующий создание освещенной сцены путем наложения нескольких полупрозрачных слоев краски. Каждый слой создается с помощью линейной интегральной свертки шума Перлина (Perlin noise), затем с помощью обратной вычитательной модели освещения (inverted substractive lighting model) вычисляется распределение толщины слоев. [9] Метод имитации акварели с помощью клеточных автоматов Этот метод описан в [21]. Рассмотрим его подробнее. Нижеописанная модель имитирует поведение воды и частиц краски с помощью клеточного автомата. Клеточный автомат представляет собой набор клеток, заданных регулярной сеткой, каждая клетка находится в каждый момент времени в определенном состоянии. С течением времени состояние каждой клетки меняется в зависимости от ее собственного состояния и состояния соседних клеток. Пожалуй, самый известный пример использования клеточных автоматов - игра Жизнь. Перенос пигмента и модель диффузии Чтобы смоделировать перенос пигмента с кисти на бумагу, а также распределение пигмента на бумаге, используется двумерный клеточный автомат на конечной сетке, представляющей поверхность бумаги. Он представляет собой массив клеток, каждая из которых способна содержать определенное количество воды и пигмента (растворенного в воде). Это похоже на лоток для приготовления льда, где краска наливается в один из контейнеров и, если контейнер полон, растекается в соседние. Итак, мы имеем следующее представление: Каждая клетка Pij имеет 2 индекса i и j Содержимое клеток описывается двумя переменными Wij и Iij, количеством воды и пигмента соответственно. Каждая клетка имеет 4-х соседей Функция перехода состояний Sij = (Wij, Iij) Sij(t + ∆t) = f (Sij(t), Sk(t) | k натуральное) Моделирование бумаги Чтобы смоделировать волокнистую структуру бумаги, добавим условие, что каждая клетка может вмещать ограниченное количество воды и пигмента. Добавим к состоянию каждой клетки 2 дополнительные переменные Bij и Cij - константы, выбранные пропорционально толщине бумаги. Bij описывает высоту "дна" клетки, а Cij максимальный объем клетки. Волокна бумаги могут моделироваться различными способами. Можно использовать псевдослучайные процессы, или использовать библиотеки предопределенных волокон. Моделирование кисти Кисть состоит из отдельных щетинок. На каждой щетинке может помещаться некоторое количество краски, и по мере рисования краска стекает на кончик кисточки, перемещается на бумагу и кисточка сохнет. Дополнительный эффект дает неравномерное распределение краски на щетинках кисти. Необычный, но эффективный способ представления кисти как второго клеточного автомата, с теми же состояниями Bij и Cij. Во время рисования краска стекает к кончику кисти, что может быть смоделировано изменением высоты дна для каждой клетки таким образом: Bij = Bij + θx. В этом выражении х - расстояние от клетки Pij до кончика кисти, а θ - константа, пропорциональная наклону кисти. Моделирование процесса рисования Процесс рисования начинается с переноса частиц воды и краски с кисти на бумагу. Клетки получают определенное количество воды и краски, далее за распределение его по бумаге отвечает функция переноса. Это происходит за 4 шага. Для каждого временного шага: 1. Перенос и диффузия частиц воды. Если клетка Pij полна воды, вода перельется в соседние клетки. В то же время часть воды может перелиться из соседних клеток в Pij. 2. Перенос частиц краски. С краской аналогично. 3. Перенос краски, чтобы сбалансировать концентрацию. После переноса воды и краски в и из соседних клеток, концентрация краски нарушилась. Изменение концентрации краски зависит коэффициента диффузии краски в воде и концентрации воды в клетках, где происходит балансировка. 4. Испарение воды. На каждом шаге часть воды испаряется, и краска сохнет. Если вся вода испарилась, частицы краски остаются в клетке Pij как сухая краска. Эти 4 шага описывают локальную функцию переноса клеточного автомата, которая выполняется для каждого временного шага. Эта модель достаточно сложна и берет в расчет некоторые свойства акварели. В зависимости от конкретной реализации кисти и функции переноса модель может быть более или менее точной, однако в любом случае она имитирует только локальное перетекание воды с пигментом, что является существенным ограничением. Метод, в основе которого - моделирование физических процессов течения воды и рассеивания пигмента. Этот метод показал в настоящее время наиболее привлекательный и реалистичный результат. [1], [16] Несмотря на то что для создания настоящих акварельных картин на компьютере лучше использовать последний подход, именно он показывает в настоящее время наиболее привлекательный и реалистичный результат, а остальные подходы моделируют лишь некоторые эффекты акварели, эти результаты тоже могут быть достаточно интересными. 9.Моделирование техники акварели Одним из довольно простых методов моделирования техники акварели (watercolor rendering) является разбиение процесса построения изображения на отдельные слои. Для каждого такого слоя вычисляется его толщина (определяющая степень его влияния) и все они последовательно наносятся на бумагу. Самым первым слоем является слой, моделирующий диффузное освещение (за исключением ярких бликов). Для определения толщины этого слоя используется модифицированный вариант модели Фонга. Толщина этого слоя определяется по следующей формуле: Следующий слой отвечает на неосвещенные части и его толщина задается следующей формулой: Последний слой представлен шумовой текстурой, служащей для моделирования мазков кистью. При это обычно эта шумовая текстура вытягивается в каком-либо направлении для получения вида вытянутых мазков, выглядящих гораздо более реалистично. Довольно простым способом композиции всех этих слоев является следующий: Для рендеринга моделей, состоящих из большого числа небольших граней, вычисление параметров слоев проще всего реализовать в вершинном шейдере. Ниже приводится реализация такого шейдера на GLSL. // // Watercolor vertex shader // uniform uniform varying varying vec3 vec3 vec3 vec3 lightPos; eyePos; diffuseThickness; unlitThickness; void main(void) { const vec3 const vec3 const vec3 const float one ambient diffuse specPower = = = = vec3 ( 1.0 ); vec3 ( 0.4, 0.4, 0.4 ); vec3 ( 0.0, 0.0, 1.0 ); 50.0; vec3 vec3 vec3 vec3 vec3 p l v h n = = = = = vec3 ( gl_ModelViewMatrix * gl_Vertex ); normalize ( lightPos - p ); normalize ( vec3 ( eyePos ) - p ); normalize ( l + v ); normalize ( gl_NormalMatrix * gl_Normal ); // compute layers thicknesses diffuseThickness = (1.0 - pow ( max ( dot ( n, h ), 0.0 ), specPower ) ) * (one diffuse); unlitThickness = (1.0 - max ( dot ( n, l ), 0.0 ) ) * (one - ambient); gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; gl_TexCoord [0] = gl_MultiTexCoord0; } Задачей фрагментного шейдера является получение значения из шумовой текстуры и смешение его с проинтерполированными параметрами слоев. Соответствующая реализация приводится ниже. // // Watercolor fragment shader // varying vec3 diffuseThickness; varying vec3 unlitThickness; uniform sampler2D noiseMap; void main (void) { vec3 color = diffuseThickness; vec3 noise = texture2D ( noiseMap, gl_TexCoord [0].xy * vec2 ( 0.7, 2.0 ) ).xyz; color = vec3 ( 1.0 ) - color * unlitThickness * noise.x; gl_FragColor = vec4 ( color, 1.0 ); } Рис 1. Чайник, изображенный в стиле акварели. Можно слегка модифицировать описанный алгоритм, "смягчая" резкие края объектов. Для этого достаточно домножить толщину диффузного слоя на max((n,v),0). Данный множитель, обращаясь в нуль на контурных линиях объекта, производит требуемое смягчение. При этом толщина диффузного слоя вычисляется по следующей формуле: Изображение, полученное при использовании смягчения краев, приводится на рис 2. Рис 2. Изображение чайника, выполненное в стиле акварели со смягчением краев. Ниже приводятся соответствующие вершинный и фрагментный шейдеры. // // Watercolor vertex shader with edge softening // uniform vec3 uniform vec3 lightPos; eyePos; varying vec3 varying vec3 diffuseThickness; unlitThickness; void main(void) { const vec3 const vec3 const vec3 const float vec3 vec3 vec3 vec3 vec3 p l v h n = = = = = one ambient diffuse specPower = = = = vec3 ( 1.0 ); vec3 ( 0.4, 0.4, 0.4 ); vec3 ( 0.0, 0.0, 1.0 ); 50.0; vec3 ( gl_ModelViewMatrix * gl_Vertex ); normalize ( lightPos - p ); normalize ( vec3 ( eyePos ) - p ); normalize ( l + v ); normalize ( gl_NormalMatrix * gl_Normal ); // compute layers thicknesses diffuseThickness = (1.0 - pow ( max ( dot ( n, h ), 0.0 ), specPower ) ) * (one - diffuse) * max ( dot ( n, v ), 0.0 ); unlitThickness = (1.0 - max ( dot ( n, l ), 0.0 ) ) * (one - ambient); gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; gl_TexCoord [0] = gl_MultiTexCoord0; } // // Watercolor fragment shader with edge softening // varying vec3 diffuseThickness; varying vec3 unlitThickness; uniform sampler2D noiseMap; void main (void) { vec3 color = diffuseThickness; vec3 noise = texture2D ( noiseMap, gl_TexCoord [0].xy * vec2 ( 0.7, 2.0 ) ).xyz; color = vec3 ( 1.0 ) - color * unlitThickness * noise.x; gl_FragColor = vec4 ( color, 1.0 ); } 9.1 Листинг программы для моделирования эффекта акварели #include "libExt.h" #include #include #include <glut.h> <stdio.h> <stdlib.h> #include #include #include #include #include "libTexture.h" "TypeDefs.h" "Vector3D.h" "Vector2D.h" "GlslProgram.h" Vector3D Vector3D unsigned unsigned unsigned unsigned unsigned Vector3D float int int bool eye ( -0.5, -0.5, 1.5 ); light ( 5, 0, 4 ); decalMap; stoneMap; teapotMap; noiseMap; paperMap; rot ( 0, 0, 0 ); angle = 0; mouseOldX = 0; mouseOldY = 0; useFilter = true; // camera position // light position // decal (diffuse) texture GlslProgram program; void startOrtho { glMatrixMode matrix glPushMatrix glLoadIdentity () // select the projection (); (); // store the projection matrix // reset the projection matrix // set up an ortho screen glOrtho glMatrixMode glPushMatrix glLoadIdentity ( GL_PROJECTION ); ( 0, 512, 0, 512, -1, 1 ); ( GL_MODELVIEW ); (); (); // select the modelview matrix // store the modelview matrix // reset the modelview matrix } void endOrtho { glMatrixMode matrix glPopMatrix matrix glMatrixMode glPopMatrix matrix } () ( GL_PROJECTION ); // select the projection (); // restore the old projection ( GL_MODELVIEW ); (); // select the modelview matrix // restore the old projection void init () { glClearColor ( 1.0, 1.0, 1.0, 1.0 ); glEnable ( GL_DEPTH_TEST ); glEnable glDepthFunc ( GL_TEXTURE_2D ); ( GL_LEQUAL ); glHint ( GL_POLYGON_SMOOTH_HINT, GL_NICEST ); glHint ( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST ); } void display () { glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); startOrtho (); glActiveTextureARB ( GL_TEXTURE0_ARB ); glBindTexture ( GL_TEXTURE_2D, paperMap ); glEnable ( GL_TEXTURE_2D ); glActiveTextureARB ( GL_TEXTURE1_ARB ); glDisable ( GL_TEXTURE_2D ); glActiveTextureARB ( GL_TEXTURE2_ARB ); glDisable ( GL_TEXTURE_2D ); glDepthMask glColor4f ( GL_FALSE ); ( 1, 1, 1, 1 ); /* glBegin ( GL_QUADS ); glTexCoord2f ( 0, 0 ); glVertex2f ( 0, 0 ); glTexCoord2f ( 1, 0 ); glVertex2f ( 511, 0 ); glTexCoord2f ( 1, 1 ); glVertex2f ( 511, 511 ); glTexCoord2f ( 0, 1 ); glVertex2f ( 0, 511 ); glEnd (); */ endOrtho (); glDepthMask ( GL_TRUE ); glActiveTextureARB ( GL_TEXTURE0_ARB ); glBindTexture ( GL_TEXTURE_2D, teapotMap ); glActiveTextureARB ( GL_TEXTURE1_ARB ); glBindTexture ( GL_TEXTURE_2D, noiseMap ); glActiveTextureARB ( GL_TEXTURE2_ARB ); glBindTexture ( GL_TEXTURE_2D, paperMap ); glActiveTextureARB ( GL_TEXTURE0_ARB ); if ( useFilter ) program.bind (); glMatrixMode ( GL_MODELVIEW ); glPushMatrix (); glRotatef glRotatef glRotatef ( rot.x, 1, 0, 0 ); ( rot.y, 0, 1, 0 ); ( rot.z, 0, 0, 1 ); glutSolidTeapot ( 0.4 ); glPopMatrix (); if ( useFilter ) program.unbind (); glutSwapBuffers (); } void reshape ( int w, int h ) { glViewport ( 0, 0, (GLsizei)w, (GLsizei)h ); glMatrixMode ( GL_PROJECTION ); glLoadIdentity (); gluPerspective ( 60.0, (GLfloat)w/(GLfloat)h, 1.0, 60.0 ); glMatrixMode ( GL_MODELVIEW ); glLoadIdentity (); gluLookAt ( eye.x, eye.y, eye.z, // eye 0, 0, 0, // center 0, 0, 1 ); // up } void key ( unsigned char key, int x, int y ) { if ( key == 27 || key == 'q' || key == 'Q' ) exit ( 0 ); if ( key == 'f' || key == 'F' ) useFilter = !useFilter; } void animate () { angle = 0.001f * glutGet ( GLUT_ELAPSED_TIME ); light.x = 2*cos ( angle ); light.y = 2*sin ( angle ); light.z = 3 + 0.3 * sin ( angle / 3 ); program.bind (); program.setUniformVector ( "eyePos", eye ); program.setUniformVector ( "lightPos", light ); program.setUniformFloat ( "time", angle ); program.unbind (); glutPostRedisplay (); } void motion ( int x, int y ) { rot.y -= ((mouseOldY - y) * 180.0f) / 200.0f; rot.z -= ((mouseOldX - x) * 180.0f) / 200.0f; rot.x = 0; if ( rot.z > 360 ) rot.z -= 360; if ( rot.z < -360 ) rot.z += 360; if ( rot.y > 360 ) rot.y -= 360; if ( rot.y < -360 ) rot.y += 360; mouseOldX = x; mouseOldY = y; glutPostRedisplay (); } void mouse ( int button, int state, int x, int y ) { // quit requested if ( state == GLUT_DOWN ) { mouseOldX = x; mouseOldY = y; } } int main ( int argc, char * argv [] ) { // initialize glut glutInit ( &argc, argv ); glutInitDisplayMode ( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH ); glutInitWindowSize ( 512, 512 ); // create window glutCreateWindow ( "OpenGL watercolor rendering" ); glutDisplayFunc glutReshapeFunc glutKeyboardFunc glutMouseFunc glutMotionFunc glutIdleFunc ( ( ( ( ( ( display reshape key mouse motion animate // register handlers ); ); ); ); ); ); init (); initExtensions (); if ( !GlslProgram :: isSupported () ) { printf ( "GLSL not supported.\n" ); return 1; } if ( !program.loadShaders ( "watercolor.vsh", "watercolor.fsh" ) ) { printf ( "Error loading shaders:\n%s\n", program.getLog ().c_str () ); return 3; } decalMap = createTexture2D ( true, "../../Textures/oak.bmp" ); stoneMap = createTexture2D ( true, "../../Textures/block.bmp" ); teapotMap = createTexture2D ( true, "../../Textures/Oxidated.jpg" ); noiseMap = createTexture2D ( true, "noise-2D.png" ); paperMap = createTexture2D ( true, "paper.dds" ); program.bind (); program.setTexture ( "noiseMap", 1 ); program.setTexture ( "paperMap", 2 ); program.unbind (); // // printf ( "Render scene in watercolor mode\n" ); printf ( "Press F key to turn watercolor mode on/off\n" ); glutMainLoop (); return 0; }