Главная -
Статьи -
Проекты -
Ссылки -
Скачать -
Из гельминтов -
Юмор, приколы -
Почитать -
Обо мне -
Мысли -
Гостевая -

Нефотореалистичный рендеринг в реальном времени. Моделирование акварели и рисования углем.

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

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

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

Все это приводит к понятию нефотореалистического рендеринга (Non-Photorealistic Rendering - NPR) - построению изображений, выглядящих нарисованными от руки (т.е. человеком). Очень хороший обзор методов и техник NPR можно найти в книге Нефотореалистичная компьютерная графика, также в разделе ссылок Вы найдете ряд ссылок на этой теме.

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

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

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

В связи с тем, что рассматриваемые реализации используют фрагменты кода и отдельные классы из готовящейся к выходу в издательстве "БХВ-Петербург" книги "Расширения OpenGL", то к данным примерам не прилагается полный исходный код на С++. Однако Вы можете скачать уже откомпилированные прогаммы и используемые шейдеры на GLSL.

Моделирование техники акварели

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

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

Толщина этого слоя определяется по следующей формуле:

diffThickness=(1-(n,h)p*Kspec)*kdiff

Следующий слой отвечает на неосвещенные части и его толщина задается следующей формулой:

unlitThickness=(1-(n,l))*kdiff

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

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

color = 1 - diffuseColor*diffuseThickness*unlitThickness*noise

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

Ниже приводится реализация такого шейдера на GLSL.

//
// Watercolor vertex shader
//

uniform vec3 lightPos;
uniform vec3 eyePos;
varying vec3 diffuseThickness;
varying vec3 unlitThickness;

void main(void)
{
    const vec3  one       = vec3 ( 1.0 );
    const vec3  ambient   = vec3 ( 0.4, 0.4, 0.4 );
    const vec3  diffuse   = vec3 ( 0.0, 0.0, 1.0 );
    const float specPower = 50.0;

    vec3 p = vec3 ( gl_ModelViewMatrix * gl_Vertex );
    vec3 l = normalize ( lightPos - p );
    vec3 v = normalize ( vec3 ( eyePos ) - p );
    vec3 h = normalize ( l + v );
    vec3 n = 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). Данный множитель, обращаясь в нуль на контурных линиях объекта, производит требуемое смягчение.

При этом толщина диффузного слоя вычисляется по следующей формуле:

diffThickness=(1-(n,h)p*Kspec)*kdiff*max((n,v),0)

Изображение, полученное при использовании смягчения краев, приводится на рис 2.

Рис 2. Изображеине чайника, выполненное в стиле акварели со смягчением краев.

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

//
// Watercolor vertex shader with edge softening
//

uniform vec3  lightPos;
uniform vec3  eyePos;

varying vec3  diffuseThickness;
varying vec3  unlitThickness;

void main(void)
{
    const vec3  one       = vec3 ( 1.0 );
    const vec3  ambient   = vec3 ( 0.4, 0.4, 0.4 );
    const vec3  diffuse   = vec3 ( 0.0, 0.0, 1.0 );
    const float specPower = 50.0;

    vec3 p = vec3 ( gl_ModelViewMatrix * gl_Vertex );
    vec3 l = normalize ( lightPos - p );
    vec3 v = normalize ( vec3 ( eyePos ) - p );
    vec3 h = normalize ( l + v );
    vec3 n = 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 );
}

В основу данной реализации легла статья Non-Photorealistic Rendering using Watercolor Inspired Textures and Illumination, авторы: Eric B. Lum, Kwan-Liu Ma.

Моделирование рисования древесным углем

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

y'=yp

В этой формуле в качестве степени p используется величина, большая единицы. Типичным значением степени является 3.5.

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

Полученная таким оразом освещенность используется для индексирования в специальную текстуру, назыаемую Contrast Enhancement Texture. На следующем рисунке приводится пример такой текстуры.

Contrast Enhancement Texture

Рис 3. Пример Contrast Enhancement Texture.

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

Однако, если t-координата для наложения СЕТ-текстуры задается явно как модифицированная освещенность, то какую величину следует взять в качестве первой текстурной координаты s ?

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

Наиболее удобным было бы взять в качестве данной текстурной координаты случайное значение.

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

Тогда, использовав обычные текстурные кординаты для обращения к такой текстуре, на выходе мы получим фактически случайное значение цвета, одну из компонент которого можно использовать в качестве текстурной координаты s для обращения к СЕТ.

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

Достаточно простым способом моделирования размывания является просто смешение (blending) текстурированного (при помощи СЕТ) изображения с нетекстурированным изображением с усиленной контрастностью.

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

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

Задачей фрагментного шейдера является наложение СЕТ, смешение текстурированной модели с нетекстурированной и наложения бумаги на получившееся значение.

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

//
// Charcoal vertex shader
//

uniform vec3 lightPos;
uniform vec3 eyePos;
varying vec3 color;

void main(void)
{
    const vec3 ambient   = vec3 ( 0.0 );
    const vec3 diffuse   = vec3 ( 1.0, 1.0, 1.0 );
    const vec3 luminance = vec3 ( 0.3, 0.59, 0.11 );

    vec3 p = vec3 ( gl_ModelViewMatrix * gl_Vertex );
    vec3 l = normalize ( lightPos - p );
    vec3 n = normalize ( gl_NormalMatrix * gl_Normal );

                                            // compute illumination
    color = ambient + diffuse * max ( dot ( n, l ), 0.0 );

                                            // apply CEO
    color = vec3 ( pow ( clamp ( dot ( color, luminance ), 0.0, 1.0 ), 3.5 ) ); 

    gl_Position     = gl_ModelViewProjectionMatrix * gl_Vertex;
    gl_TexCoord [0] = gl_MultiTexCoord0;
}

//
// Charcoal fragment shader
//

varying vec3       color;

uniform sampler2D  randomTex;
uniform sampler2D  cetTex;
uniform sampler2D  paperTex;

void main (void)
{
    vec4 random   = texture2D ( randomTex,  gl_TexCoord [0].xy );
    vec3 contrast = texture2D ( cetTex, vec2 ( random.x, 1.0 - color.x ) ).rgb;
    vec3 smudge   = 0.5 * (contrast + color);	
    vec3 paper    = texture2D ( paperTex, gl_FragCoord.xy / 512.0 ).rgb;

    gl_FragColor.rgb = contrast + vec3 ( 1.0 ) - paper;
    gl_FragColor.a   = 1.0;
}

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

Рис 4. Изображение в технике рисования углем по бумаге.

Для получения Contrast Enhancement Texture использовался простой скрипт на языке Python, приводимый на слдующем листинге.

#
# Script to create contrast enhancement texture
#

import PIL, Image, random, math, ImageEnhance

                                # basic parameters: bitmap size and contrast power
size         = 512
contrastExp  = 15
noiseDensity = 3.0

im   = Image.new ( "RGB", (size, size) )

                                # fill it with white
for i in range (size):
    for j in range (size):
        im.putpixel ( (i,j), ( 255, 255, 255 ) )


numBlackPixels = int( noiseDensity * float(size) * float(size) )

for i in range ( numBlackPixels) :
    x = random.random ()
    y = random.random ()

                                # apply contrast enhansment
    y = math.pow ( y, contrastExp )

    px = int(x*(size-1))
    py = size - 1 - int(y*(size-1))
    if py < 0:
       py = 0

    if py >= size:
       py = size - 1

    im.putpixel ( (px, py), (0, 0, 0))

sh = ImageEnhance.Sharpness ( im )
im2 = sh.enhance ( 0.2 )
im2.show ()
im2.save ( "cet.bmp", "bmp" )

Следующий скрипт используется для построения текстуры, состоящей из случайных значений.

#
# Script to random texture
#

import PIL, Image, random

                                # basic parameters: bitmap size
size = 256
im   = Image.new ( "RGB", (size, size) )

for i in range (size):
    for j in range (size):

        r = int ( 255.0 * random.random () )
        g = int ( 255.0 * random.random () )
        b = int ( 255.0 * random.random () )

        im.putpixel ( (i,j), ( r, g, b ) )

im.show ()
im.save ( "random.bmp", "bmp" )

Алгоритм был взят из статьи "Hardware Accelerated Real Time Charcoal Rendering" написанной Aditi Majumder и M. Gopi.

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

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


Copyright © 2005 Алексей В. Боресков

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