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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ниже приводится реализация такого шейдера на 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 );
}

watercolor rendering screenshot

Рис 1. Чайник, изображенный в стиле акварели.

Можно слегка модифицировать описанный алгоритм, "смягчая" резкие края объектов. Для этого достаточно домножить толщину диффузного слоя на max((n,v),0). Данный множитель, обращаясь в нуль на контурных линиях объекта, производит требуемое смягчение.

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

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

watercolor rendering screenshot

Рис 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.

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

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

В этой формуле в качестве степени 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;
}

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

charcoal screenshot

Рис 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.

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

Valid HTML 4.01 Transitional

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