7 Создание заказных шейдеров и текстур с помощью Pynodes

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

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

В этой главе мы изучим:

• Как писать Pynodes, которые создают простые цветные узоры

• Как писать Pynodes, которые производят узоры с нормалями

• Как писать анимированные Pynodes

• Как писать материалы, зависимые от высоты и наклона

• Как создавать шейдеры, которые реагируют на угол падающего света

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




Материалы, шейдеры, и текстуры - термины, которые часто используются как синонимы, хотя между ними есть разница в значении. Для наших целей мы попытаемся придерживаться следующих определений: текстура является основным строительным блоком, например, цветной или нормальный узор или просто некоторая функция, которая возвращает значение в зависимости от позиции на поверхности. Шейдер принимает на вход любое количество текстур или просто базовый цвет и возвращает цвет, основанный на влиянии падающего света и, возможно, направления вида. Материал — это набор текстур, шейдеров, и всех типов свойств, которые могут быть приложены к объекту. Pynodes могут быть текстурами, а также шейдерами.


Основы

Когда мы разрабатываем Pynode, мы в основном разрабатываем нечто, что предоставляет функцию, которая вызывается для каждого пикселя на экране, который должен быть затенен (shaded) этим нодом (или даже неоднократно, если включен oversampling). Эта функция получает, кроме прочего, координаты x, y, и z точки на затеняемом объекте, которая соответствует пикселю на экране, который мы к настоящему времени вычисляем. Затем функция должна вернуть что-то полезное, такое как цвет, значение интенсивности, или что-то чуть менее интуитивное, например, нормаль.

В окне редактора Нодов Блендера каждый нод материала, включая Pynode, представлен прямоугольником, который имеет входы слева и выходы справа. Эти входы и выходы, часто называемые сокетами, представлены небольшими цветными кругами (смотрите следующий скриншот). Эти сокеты можно использовать для связи нодов вместе; щелкая по выходному сокету одного нода и перетаскивая мышь ко входному сокету другого нода, эти ноды будут связаны. Так, комбинируя требуемым образом множество различных нодов, можно создать очень сложные и мощные шейдеры.


От нодов к Pynodes

Сила системы Нодов Блендера проистекает не только из её многочисленных встроенных типов нодов, и множества способов, которыми эти ноды могут быть связаны, но также из того, что мы можем написать новые ноды на Питоне, которые можно связывать так же, как обычные ноды.

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

from Blender import Node

class MyNode(Node.Scripted):

  def __init__(self, sockets):

    sockets.input  = [Node.Socket('Coords',

              val= 3*[1.0])]

    sockets.output = [Node.Socket('Color', 

              val = 4*[1.0])]

  def __call__(self):

    x,y,z = self.input.Coords

    self.output.Color = [abs(x),abs(y),abs(z),1.0]

Прежде чем мы посмотрим на этот код подробно, попробуем его в Блендере, чтобы посмотреть, как он работает на практике:

1. Откройте новый файл в текстовом редакторе и дайте ему значимое имя.

2. Скопируйте код примера.

3. Создайте простую сцену, например, простую UV-сферу в начале координат с парой ламп и камерой.

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

5. Наконец, добавьте динамический (Dinamic) нод в Нодовом редакторе (Add | Dynamic) и выберите имя файла, который Вы отредактировали, щелчком на кнопке выбора динамического нода и выбрав файл из списка.

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



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

Теперь вернёмся к коду.

На первой строке мы импортируем модуль Node из Блендера, поскольку мы создаём новый тип нода, но основное его поведение уже определено в модуле Node.

Затем мы определяем класс MyNode, подкласс Node.Scripted, который будет вести себя просто подобно ноду Scripted, за исключением тех частей, которые мы переопределим.

Затем, мы определяем функцию __init__(), которая будет вызваться первый раз при создании этого типа Pynode в редакторе нодов, или всякий раз, когда мы щелкаем на кнопку Update. Когда это случается, Блендер передаёт два аргумента в эту функцию: self, ссылку на нод, который мы используем, и sockets, ссылку на объект, которая будет указывать на наши списки входных и выходных сокетов. С их помощью ноды в редакторе нодов получают данные на вход или посылают их дальше.

На выделенной строке мы определяем список определений входных сокетов; в нашем случае только один с названием Coords. Это - векторный вход, поскольку инициализируется списком трех чисел с плавающей точкой, который определяет значение по умолчанию, если этот входной сокет не подключен к другому ноду. Векторные сокеты представлены как синие круги в нодовом редакторе.

Другие типы входного сокета также возможны и этот тип определяется величиной аргумента val. Выходные сокеты определяются так же. Список трех чисел с плавающей точкой определяет векторный сокет, список четырех чисел - цветовой сокет (с красным, зеленым, синим, и альфа компонентом), а сокет, представляющий простое значение, как, например, интенсивность, инициализируется единственным числом. Заметьте, что мы не можем отличать входы, которые должны заполняться пользователем от тех, что должны быть подключены к другому ноду. Мы используем входные сокеты для них обоих и должны подтверждать их предполагаемое использование. К настоящему времени, нет средства добавлять кнопки или другие управляющие элементы на Pynode.

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

Затем мы определяем функцию __call__(), которая вызывается всякий раз при затенении пикселя. Она не принимает никаких аргументов, но self - это ссылка на текущий нод, которая используется в следующих строках для получения доступа к входному и выходному сокетам.

В теле функции __call__() мы извлекаем три компонента из входного сокета с названием Coords и назначаем их переменным, которые легко запомнить. Наконец, мы создаем новый четырехкомпонентный список, который представляет наш рассчитанный цвет и назначаем его выходному сокету с названием Color.

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


Регулярное заполнение

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

from Blender import Node,Noise,Scene

from math import sqrt,sin,cos,pi,exp,floor

from Blender.Mathutils import Vector as vec

# создаёт регулярное заполнение для использования в 

качестве цветовой карты

class Tilings(Node.Scripted):

  def __init__(self, sockets):

    sockets.input = [Node.Socket('type' , 

            val= 2.0, min = 1.0, max = 3.0),

            Node.Socket('scale' ,

            val= 2.0, min = 0.1, max = 10.0),

            Node.Socket('color1', 

              val= [1.0,0.0,0.0,1.0]),

            Node.Socket('color2', 

              val= [0.0,1.0,0.0,1.0]),

            Node.Socket('color3', 

              val= [0.0,0.0,1.0,1.0]),

            Node.Socket('Coords', 

              val= 3*[1.0])]

    sockets.output = [Node.Socket('Color', 

                   val = 4*[1.0])]

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

Мы также определяем вход Coords. Этот входной сокет может перехватывать любой выход сокета геометрии. Таким образом у нас есть множество возможностей отобразить нашу цветную текстуру на объект, который мы текстурируем. Сокет Scale определяется также, чтобы управлять размером нашей текстуры.

Наконец, мы определяем сокет Type, чтобы выбирать узор, который мы хотим генерировать. Так как API для Pynode не обеспечивает выпадающих меню или любого другого простого управляющего элемента для выбора, мы делаем сокет с одиночным значением и произвольно выбираем величины, представляющие наш выбор: 1.0 для треугольников, 2.0 для шахматного поля, и 3.0 для шестиугольников.

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

    self.cos45 = cos(pi/4)

    self.sin45 = sin(pi/4)

    self.stretch = 1/sqrt(3.0)

    self.cmap = { (0,0):None,(0,1):2,  (0,2):0, 

           (1,0):0,  (1,1):1,  (1,2):None, 

           (2,0):2,  (2,1):None,(2,2):1 }

Следующим шагом будет определение функции __call__():

  def __call__(self):


    tex_coord = self.input.Coords

    # мы игнорируем любую z-координату

    x = tex_coord[0]*self.input.scale 

    y = tex_coord[1]*self.input.scale


    c1 = self.input.color1

    c2 = self.input.color2

    c3 = self.input.color3


    col= c1

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

    if self.input.type<= 1.0:

     col = self.triangle(x,y,c1,c2)

    elif self.input.type <= 2.0:

     col = self.checker(x,y,c1,c2)

    else:

     col = self.hexagon(x,y,c1,c2,c3)


    self.output.Color = col

Все различные функции генерации узоров очень похожи; они берут координаты x и y и два или три цвета в качестве аргументов и возвращают единственный цвет. Так как это функции-члены класса, они также принимают дополнительный первый аргумент self.

  def checker(self,x,y,c1,c2):

    if int(floor(x%2)) înt(floor(y%2)):

     return c1

    return c2

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

  def triangle(self,x,y,c1,c2):

    y *= self.stretch

    x,y = self.cos45*x - self.sin45*y, 

       self.sin45*x + self.cos45*y

    if int(floor(x%2)) înt(floor(y%2)) ^ \

     int(y%2>x%2) : return c1

    return c2

Функция triangle сначала одновременно вращает как x, так и y координаты на угол 45 градусов (превращение квадратов в вертикальные ромбы). Затем она определяет цвет, основываясь на номерах строки и колонки в точности подобно функции checker, но с уловкой: третье условие (выделено) проверяет, слева ли мы от диагонали, пересекающей квадрат, и поскольку мы вращали нашу сетку, на самом деле мы проверяем действительно ли координаты выше горизонтальной линии, делящей наш ромб. Это может звучать немного сложным, но Вы можете посмотреть на следующую иллюстрацию, чтобы понять идею:



  def hexagon(self,x,y,c1,c2,c3):

    y *= self.stretch

    x,y = self.cos45*x - self.sin45*y, 

       self.sin45*x + self.cos45*y

    xf = int(floor(x%3))

    yf = int(floor(y%3))

    top = int((y%1)>(x%1))

    c = self.cmap[(xf,yf)]

    if c == None:

     if top :

       c = self.cmap[(xf,(yf+1)%3)]

     else :

       c = self.cmap[(xf,(yf+2)%3)]

    return (c1,c2,c3)[c]

Функция hexagon (шестиугольник) во многих отношениях похожа на функцию triangle (в конце концов шестиугольник - шесть треугольников, склеенных вместе). Следовательно, в ней применяется та же хитрость с вращением, но, вместо выбора цвета с использованием простой формулы, здесь всё несколько сложнее, и, следовательно, мы используем цветовую карту (выделено в предыдущем фрагменте кода). В основном, мы делим экран на горизонтальные и вертикальные полосы, и выбираем цвет, основываясь на том, в какую полосу мы попали.

Последняя часть магии - на последней строке нашего скрипта:

__node__ = Tilings

В текущей реализации Pynodes, Блендеру нужно это присвоение, чтобы идентифицировать класс в качестве нода. Наш нод появится в выпадающем меню скриптовых нодов как Tilings. Полный код доступен как tilings.py в файле tilings.blend вместе с примером нодовой сети. Некоторые возможные узоры показаны на следующем скриншоте:



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



Anti-aliasing

Если вы посмотрите внимательно на диагональные границы шестиугольного или треугольного узора, вы должны обратить внимание на некоторые артефакты наподобие лестницы, даже если oversampling был установлен на большое значение.

Сам Блендер достаточно умён, чтобы прилагать выбранный уровень anti-aliasing, например, к границам объектов, но в большинстве случаев текстуры на поверхности должны самостоятельно заботиться о наложении anti-aliasing. Встроенные текстуры Блендера, конечно, разработаны именно таким образом, но наши собственные текстуры, произведенные с помощью Pynodes, должны заниматься этим явно.

Существуют многочисленные математические методы, позволяющие уменьшить aliasing в сгенерированных текстурах, но большинство из них не так просто осуществить, или они требуют специфических знаний о способе генерации узора. К счастью, Блендер предоставляет нам опцию Full OSA (окно Кнопок | контекст Затенения | кнопки Материала | панель Links and pipeline). Если мы включим эту опцию, Блендер будет вынужден производить oversample с каждым пикселем в нашей текстуре в количестве, выбранном в кнопках рендера. Это дорогой вариант, но он позволит отделаться от эффектов aliasing без необходимости осуществлять специфические параметры фильтрации в нашем текстурном Pynode.


Индексирование текстуры вектором

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

Мы не можем просто подключить цветную текстуру к цветовым входам, так как это приведёт, может быть, к интересной модели, но каждая плитка не будет иметь однородной окраски. Решением будет модифицировать наш Pynode, чтобы производить уникальный вектор, который станет однородным в пределах любой данной плитки. Этот вектор затем может быть подключен к любой шумовой текстуре, которая принимает вектор на входе, так же, как делают все текстуры Блендера. Этот вектор используется нодом текстуры шума, чтобы указывать на единственную точку в произвольной текстуре, и таким способом мы можем произвести произвольно окрашенные, но однородные элементы.

Чтобы обеспечить такую функциональность, мы модифицируем наш код, удалив цветовые входы, и заменяя цветовой выход векторным выходом (не показано). Код в функции __call__() теперь должен будет производить вектор вместо цвета. Здесь мы покажем модифицированную функцию triangle (полный код доступен как tilingsv.py в файле tilingsv.blend):

def triangle(self,x,y):

    y *= self.stretch

    x,y = self.cos45*x - self.sin45*y, 

       self.sin45*x + self.cos45*y

    if int(floor(x%2)) înt(floor(y%2)) ^ \

     int(y%2>x%2) :

     return [floor(x),floor(y),0.0]

    return [floor(x)+0.5,floor(y),0.0]

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

Результаты показывают произвольный узор из треугольников, которые следуют за большими корреляциями шума оставляя каждый индивидуальный треугольник с однородным цветом. Образец справа имеет больший размер шума в использованной текстуре cloud:



Возможная настройка нодов показана на следующем скриншоте:



Свежий бриз - текстуры с нормалями

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

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

Длина волн ряби управляется еще одним входом с названием wavelength, и наш нод Ripples (Пульсации) будет также иметь входной сокет для координат.

Четвертый и последний вход, называемый direction - вектор, который контролирует ориентацию наших элементарных волн. Может быть установлено вручную пользователем, но при желании, может быть соединён с нодом normal, который предоставляет простой способ манипулировать направлением с помощью мыши.

Окончательная настройка нодов, которая объединяет все это, показана на скриншоте редактора нодов:



Скрипт для нода прост; после нескольких необходимых операций импорта мы определим многочисленные входные сокеты и наш единственный выходной сокет.

from Blender import Node

from math import cos

from Blender.Mathutils import Vector as vec

class Ripples(Node.Scripted):

  def __init__(self, sockets):

    sockets.input = [

     Node.Socket('amplitude' , val= 1.0,  

            min = 0.001, max = 1.0),

     Node.Socket('wavelength', val= 1.0,  

            min = 0.01, max = 1000.0),

     Node.Socket('direction' , val= [1.0,0.0,0.0]),

     Node.Socket('Coords'   , val= 3*[1.0])]

    sockets.output = [Node.Socket('Normal', 

             val = [0.0,0.0,1.0])]


  def __call__(self):


    norm = vec(0.0,0.0,1.0)


    p = vec(self.input.Coords)

    d = vec(self.input.direction)

    x = p.dot(d)*self.input.wavelength

    norm.x=-self.input.amplitude*cos(x)


    n = norm.normalize()


    self.output.Normal = n*.01


__node__ = Ripples

Снова, вся реальная работа выполняется в функции __call__() (выделено в предыдущем куске коде). Мы сначала определяем сокращения p и d для векторов координат и направления соответственно. Наши элементарные волны - функции синуса и позиция на этой синусоиде определяется проекцией позиции на вектора направления. Эта проекция вычисляется скалярным произведением - операция предоставлена методом dot() объекта Vector.



Затем, проекция умножается на длину волны. Если бы мы вычислили синус, у нас была бы высота нашей волны. Но нас, тем не менее, интересует не высота, а нормаль. Нормаль всегда направлена вверх и перемещается вместе с нашей сунусоидальной волной (смотри следующую диаграмму). Можно показать, что эта нормаль - вектор с z-компонентой 1.0 и x-компонентой, равной отрицательной производной функции синуса, то есть, минус косинус. Скрипт (ripples.py) и пример настройки нодов доступны как файл ripples.blend.



В нодовой сети, которую мы показывали раньше, Вы могли обратить внимание, что вместо связи нода геометрии непосредственно с нашим нодом ripples, мы добавили второй нод текстуры, и скомбинировали этот нод с вводом геометрии сложив с масштабированным выходом normal текстурного нода. Мы могли бы смешать с некоторым шумом в ноде ripples непосредственно, но этим способом мы даем значительно больше управления пользователю над типом и количеством шума, который он хочет добавить (если хочет). Это - обычная модель: ноды должны разрабатываться по возможности простыми, чтобы облегчить их использование многократно с различными настройками.

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




Капли - анимированные Pynodes

Множество узоров не являются статическими, а изменяются во времени. Одним из примеров являются пульсации, сформированные каплями, падающими в пруд. Блендер представляет параметры времени рендера, такие как, например, стартовый кадр, частота кадров, и текущий кадр, так что у нас есть много зацепок, чтобы сделать наши Pynodes зависимыми от времени. Мы увидим как использовать эти зацепки в скрипте, который генерирует рисунок капель. Узор, который изменяется достоверно, имеет сходство с расширяющимися волнами, вызванными каплями, падающими в пруд. На пути мы также приобретём несколько полезных хитростей, чтобы ускорить вычисления, сохраняя результаты дорогих вычислений в самом Pynode, чтобы позже многократно их использовать.


Параметры времени рендера

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

scn        = Scene.GetCurrent()

context      = scn.getRenderingContext()

current_frame   = context.currentFrame() #Текущий кадр

start_frame    = context.startFrame()  #Начальный кадр

end_frame     = context.endFrame()   #Конечный кадр

frames_per_second = context.fps       #Частота

                      #кадров, fps

Теперь, с этой информацией, мы можем вычислить время, или абсолютное, или относительно стартового кадра:

absolute_time = current_frame/float(frames_per_second)

relative_time = (current_frame-start_frame)/ \ 

         float(frames_per_second)

Заметьте преобразование во float (число с плавающей точкой) в знаменателе (выделено). Этим способом мы гарантируем, чтобы деление рассматривалось как операция с плавающей точкой. Не строго необходимо, поскольку fps возвращается с типом плавающей точки, но множество людей считают частоту кадров как некоторую целую величину, например, 25 или 30. Тем не менее, так бывает не всегда (например, кодировка NTSC использует дробную частоту кадров), так что мы лучше сделаем это явно. Также заметьте, что мы не можем покончить с этим делением, в противном случае, когда люди захотят изменить своё решение о выбранной частоте кадров, скорость анимации должна измениться.


Всё, что выглядит хорошо — это хорошо

Точно имитировать то, как выглядят пульсации, вызванные падением капелек, может показаться трудным, но это просто, хотя и немного запутано. Читатели, интересующиеся базовой математикой, могут проверить какие-нибудь ссылки (например, http://en.wikipedia.org/wiki/Wave). Нашей целью, тем не менее, не является моделирование реального мира с максимально возможной точностью, а обеспечение художника текстурой, которая выглядит хорошо и управляется так, чтобы текстуру можно было применить даже в нереалистичных ситуациях.

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



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

position_of_maximum=speed*time

damping = 1.0/(1.0+dampf*position_of_maximum)

distance = sqrt((x-dropx)**2+(y-dropy)**2)

height = damping*a*exp(-(distance-

position_of_maximum)**2/c)* \ 

     cos(freq*(distance-position_of_maximum))

Здесь, dropx и dropy - позиция ударившей капли, a - наш регулируемый параметр высоты.

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


Хранение дорогостоящих результатов для многократного использования

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

Мы должны были бы делать это каждый раз при вызове метода __call__() (то есть, для каждого видимого пикселя в нашей текстуре). Тем не менее, это было бы огромными тратами процессорных сил, поскольку вычисление множества случайных чисел и получение и возврат памяти для, возможно, многих капель дорого.

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

class MyNode(Node.Scripted):


  def __init__(self, sockets):

    sockets.input  = [Node.Socket('InputParam', 

             val = 1.0)]

    sockets.output  = [Node.Socket('OutputVal' , 

             val = 1.0)]

    self.InputParam = None

    self.Result   = None

  def __call__(self):

    if self.InputParam == None or \

     self.InputParam != self.input.InputParam :

     self.InputParam = self.input.InputParam

     self.Result   = интенсивные_вычисления ...

    self.output.OutputVal = другие_вычисления …

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


Вычисление нормалей

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

В противовес нодам материалов, ноды текстур Блендера обеспечивают преобразующую функцию, называемую 'Value to Normal' (величина в нормаль), которая доступна в нодовом редакторе текстур из меню Add|Convertor|Value to Normal.

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

Пока мы можем оценивать функцию в трех точках: f(x,y),f(x+nabla,y), и f(x,y+nabla), мы можем оценить направление нормали в x,y, изучая наклон нашей функции в направлениях x и y. Нормаль поверхности будет вектором, перпендикулярным к плоскости, определенной этими двумя наклонами. Мы можем взять любую малую величину для nabla, чтобы попробовать с ней, и если это не будет выглядеть хорошо, мы можем её уменьшить.


Собираем всё это вместе

Взяв все эти идеи из предыдущих параграфов, мы можем приготовить следующую программу для нашего Pynode Raindrops (с опущенными операторами import):

class Raindrops(Node.Scripted):

  def __init__(self, sockets):

    sockets.input = [

     Node.Socket('Drops_per_second'  ,

           val = 5.0, min = 0.01, max = 100.0),

     Node.Socket('a',val=5.0,min=0.01,max=100.0),

     Node.Socket('c',val=0.04,min=0.001,max=10.0),

     Node.Socket('speed',val=1.0,min=0.001, max=10.0),

     Node.Socket('freq',val=25.0,min=0.1, max=100.0),

     Node.Socket('dampf',val=1.0,min=0.01, max=100.0),

     Node.Socket('Coords', val = 3*[1.0])]

    sockets.output = [

        Node.Socket('Height', val = 1.0),

        Node.Socket('Normal', val = 3 *[0.0])]

    self.drops_per_second = None

    self.ndrops = None

Код инициализации определяет множество входных сокетов помимо координатного. Drops_per_second (капель в секунду) должен быть самочитаемым. a и c - общая высота и ширина пульсаций, двигающихся наружу из точки удара. speed и freq определяют, как быстро наши пульсации двигаются и насколько близко волны друг к другу. То, как быстро высота волн уменьшается во время пути наружу, определяет dampf.

Мы также определяем два выходных сокета: Height будет содержать рассчитанную высоту и Normal будет содержать соответствующую нормаль в этой же точке. Normal - это то, что Вы должны обычно использовать для получения поверхностного эффекта распространения, но рассчитанная высота может быть полезной, например, чтобы смягчить величину отражательной способности поверхности.

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

Определение функции __call__() начинается с инициализации множества локальных переменных. Одно примечательное место - то, где мы установили произвольное семя, используемое функциями модуля Noise (выделено в следующем коде). Таким образом, мы убеждаемся, что всякий раз, когда мы пересчитываем точки удара, мы получаем повторяемые результаты, что если мы установили бы количество капель в секунду сначала на десять, а позже на двадцать, и, затем ввернулись к десяти, сгенерированный узор будет тем же. Если Вы хотели бы изменить это, Вы могли бы добавить дополнительный входной сокет, который нужно использовать как вход для функции setRandomSeed():

  def __call__(self):


    twopi = 2*pi


    col = [0,0,0,1]

    nor = [0,0,1]

    tex_coord = self.input.Coords

    x = tex_coord[0] 

    y = tex_coord[1]


    a = self.input.a

    c = self.input.c

    Noise.setRandomSeed(42)


    scn        = Scene.GetCurrent()

    context      = scn.getRenderingContext()

    current_frame   = context.currentFrame()

    start_frame    = context.startFrame()

    end_frame     = context.endFrame()

    frames_per_second = context.fps

    time = current_frame/float(frames_per_second)

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

    drops_per_second = self.input.Drops_per_second

    # вычисление числа капель для генерации

    # в период анимации

    ndrops = 1 + int(drops_per_second * \

        (float(end_frame) – start_frame+ 1)/ \

        frames_per_second )


    if self.drops_per_second != drops_per_second \

    or self.ndrops != ndrops:

     self.drop = [ (Noise.random(), Noise.random(),

       Noise.random() + 0.5) for i in range(ndrops)]

     self.drops_per_second = drops_per_second

     self.ndrops = ndrops

Если мы должны вычислить позиции капель заново, мы назначаем список кортежей в переменную экземпляра self.drop, каждый из которых состоит из координат x и y позиции капли и случайного размера капли, от которой будет зависеть высота волн.

Строк оставшейся части полностью выполняются всякий раз при вызове __call__(), но выделенная строка показывает значимую оптимизацию. Поскольку капли, которые еще не упали в текущем кадре, не привносят изменений высоты, мы исключаем их из вычисления:

    speed=self.input.speed

    freq=self.input.freq

    dampf=self.input.dampf


    height = 0.0

    height_dx = 0.0

    height_dy = 0.0

    nabla = 0.01

    for i in range(1+int(drops_per_second*time)):

     dropx,dropy,dropsize = self.drop[i]

     position_of_maximum=speed*time- \

       i/float(drops_per_second)

     damping = 1.0/(1.0+dampf*position_of_maximum)

     distance = sqrt((x-dropx)**2+(y-dropy)**2)

     height += damping*a*dropsize* \

       exp(-(distance-position_of_maximum)**2/c)* \

       cos(freq*(distance-position_of_maximum))

     distance_dx = sqrt((x+nabla-dropx)**2+ \

               (y-dropy)**2)

     height_dx += damping*a*dropsize* \

       exp(-(distance_dx-position_of_maximum)**2/c) \ 

       * cos(freq*(distance_dx-position_of_maximum))

     distance_dy = sqrt((x-dropx)**2+ \

               (y+nabla-dropy)**2)

     height_dy += damping*a*dropsize* \

       exp(-(distance_dy-position_of_maximum)**2/c) \

       *cos(freq*(distance_dy-position_of_maximum))

В предыдущем коде мы действительно вычисляем высоту в трех различных позициях, чтобы получить возможность аппроксимировать нормаль (как объяснено раньше). Эти величины используются в следующих строках, чтобы определить x и y компоненты нормали (z компонента установлена в единицу). Сама рассчитанная высота делится на количество капель (таким образом, средняя высота не изменится при изменении количества капель) и на общий коэффициент масштабирования a, который может быть задан пользователем прежде, чем будет подсоединён выходной сокет (выделено):

    nor[0]=height-height_dx

    nor[1]=height-height_dy


    height /= ndrops * a

    self.output.Height = height


    N = (vec(self.shi.surfaceNormal)+0.2 * \

      vec(nor)).normalize()

    self.output.Normal= N


__node__ = Raindrops

Рассчитанная нормаль затем добавляется к поверхностной нормали того пикселя, который мы вычисляем, таким образом, волны будут все еще хорошо выглядеть на искривленной поверхности, и нормируется перед назначением её в выходной сокет. Последняя строка как обычно определяет значимое имя для этого Pynode. Полный код и пример настройки нодов доступны как raindrops.py в файле raindrops.blend. Пример кадра из анимации показан на следующем скриншоте:



Пример нодовой сети показан на следующем скриншоте:



Грозовой перевал — материал, зависимый от наклона

В Блендере очень просто генерировать фрактальную местность (просто добавьте плоскость, перейдите в режим редактирования, выберите всё, затем несколько раз подразделите фрактально W > 3). Если Вы хотите чего-то большего, Вам в помощь существует несколько отлично разработанных скриптов (посмотрите, например, http://sites.google.com/site/androcto/Home/python-scripts/ANTLandscape_104b_249.py). Но как Вы наложите текстуры на такую местность? В этом примере мы изучим метод, выбирающий между различными входами материала, основываясь на величине угла наклона поверхности, которую мы затеняем. Это позволит нам создать эффект, при котором очень крутые откосы обычно лишены зелени, даже если они оказались ниже линии деревьев. В комбинации с высото-зависимым материалом мы сможем затенить гористую местность достаточно убедительно.


Уменьшение времени вычислений:

Pynodes в вычислительном отношении затратны, так как они вызываются для каждого видимого пикселя. Умное программирование может иногда уменьшить количество необходимых вычислений, но если требуется дальнейшее ускорение, может помочь компилятор-на-лету (just-in-time compiler). psyco является таким компилятором и, мы столкнемся с ним в последней главе, где мы будем применять его на Pynodes и посмотрим, имеет ли он какой-либо заметный эффект.


Определение уклона

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



Поскольку мы принимаем нашу (воображаемую) плоскость пола вытянутой горизонтально вдоль осей x и y, этот угол полностью определяется z-компонентой нормали к поверхности в этой же точке. Теперь мы можем вычислить этот угол точно (это arcsin(z/√x2+y2) ), но, как художникам, нам, возможно, в любом случае захочется иметь некоторое дополнительное управление, таким образом мы просто берем нормализованную z-компоненту нормали к поверхности и изменяем эту выходную интенсивность с помощью любого нода color ramp, который нам нравится. В пределах Pynode, нормаль поверхности является легко доступным вектором: self.input.shi.surfaceNormal. Однако есть препятствие...


Мировое пространство против пространства камеры

Нормаль поверхности, которую мы имеем в распоряжении, определена в пространстве камеры. Это означает, что, например, когда нормаль поверхности указывает прямо в камеру, она определена как (0, 0,-1). В данный момент мы хотим определить нашу нормаль поверхности в мировом пространстве. Нормаль, которая указывает прямо вверх, например, должна иметь величину (0,0,1) независимо от позиции или наклона камеры (в конце концов, растительность на горном склоне обычно не изменяется с изменением угла камеры). К счастью, мы можем провести преобразование из пространства камеры в мировое пространство, взяв матрицу камеры мирового пространства и умножив нормаль поверхности на вращающую часть этой матрицы. Результирующий код выглядит похожим на это:

class Slope(Node.Scripted):

  def __init__(self, sockets):

    sockets.output = [Node.Socket('SlopeX', val = 1.0), 

             Node.Socket('SlopeY', val = 1.0), 

             Node.Socket('SlopeZ', val = 1.0),]

    self.offset =  vec([1,1,1])

    self.scale =  0.5

Заметьте, что код инициализации не определяет входных сокетов. Мы получим нормаль поверхности в позиции пикселя, который мы затеняем, из входа shader (выделено в следующей части кода). Мы определяем три отдельных выходных сокета для x, y, и z компонент наклона для удобства использования в нодовой сети. Так как мы, по большей части, используем именно z-компоненту наклона, то если мы будем иметь её доступной в отдельном сокете, нам не придётся использовать для её извлечения из вектора дополнительный нод обработки вектора.

  def __call__(self):


    scn=Scene.GetCurrent()

    cam=scn.objects.camera

    rot=cam.getMatrix('worldspace').rotationPart(

               ).resize4x4();

    N = vec(self.shi.surfaceNormal).normalize(

               ).resize4D() * rot

    N = (N + self.offset ) * self.scale

    self.output.SlopeX=N[0]

    self.output.SlopeY=N[1]

    self.output.SlopeZ=N[2]


__node__ = Slope

Преобразование из пространства камеры в мировое пространство делается в строке, которая ссылается на нормаль поверхности (выделено). Ориентация зависит только от вращения, следовательно, мы извлекаем вращающую часть матрицы преобразования камеры до того, как мы умножим нормаль поверхности на неё. Так как нормализованный результат может указывать вниз, мы заставляем z-компоненту находиться в дипазоне [0, 1], прибавляя 1 и умножая на 0.5. Полный код доступен как slope.py в файле slope.blend.

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

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



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




Мыльные пузыри — шейдер, зависимый от точки зрения

У некоторых материалов вид меняется в зависимости от угла, под которым мы на них смотрим. Перья птиц, некоторые причудливые автомобильные краски, нефтяные разливы на воде, и мыльные пузыри - вот несколько примеров. Этот феномен изменения цветов известен как радужность (iridescence). Если мы хотим осуществить нечто подобное, нам нужен доступ к вектору вида и нормали поверхности. В нашем шейдере мыльного пузыря мы увидим один из способов сделать это.

Сначала немного математики: Почему это мыльные пузыри показывают все эти различные цвета? Мыльные пузыри - это в основном искривлённые водяные плёнки (с небольшим количеством мыла), и свет отражается от поверхности раздела между воздухом и водой. Следовательно, падающий луч частично отражается, когда он попадает на внешнюю поверхность пузыря, и отражается снова, когда он достигает внутренней поверхности. Следовательно, отраженный свет, который попадает в глаз — является суммой света, прошедшего различные расстояния; часть его прошла дополнительное расстояние в две толщины мыльного пузыря.

Теперь учтём, что свет ведет себя подобно волне, а волны, которые интерферируют, могут или ослаблять или усиливать друг друга в зависимости от их фазы, и поэтому, два световых луча, прошедшие расстояния, разница которых не кратна в точности их длине волны, гасят друг друга. В результате, белый свет (континуум, совокупность цветов), отраженный мыльным пузырём с толщиной, равной половине длины волны некоторого специфического цвета, покажет только этот единственный цвет, поскольку все остальные цвета подавлены, так как они "не соответствуют" должным образом толщине между внутренней и внешней поверхностью. (Существует гораздо больше информации о мыльных пузырях. Для большей и более точной информации вот ссылка: http://ru.wikipedia.org/wiki/Мыльные_пузыри.)



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

Вся эта информация переводится в удивительно короткую часть кода (полный код доступен как irridescence.py в файле irridescence.blend вместе с примером нодовой сети).

Наряду с координатами, у нас есть ещё два входных сокета — один для толщины водяной плёнки и один для вариаций. Вариации будут добавляться к толщине и этот сокет может быть присоединён к текстурному ноду, чтобы генерировать вихри и тому подобное. У нас есть единственный выходной сокет для рассчитанного расстояния: class Iridescence(Node.Scripted):

  def __init__(self, sockets):

    sockets.input = [ 

     Node.Socket('Coords', val= 3*[1.0]), 

     Node.Socket('Thickness', val=275.0, 

            min=100.0, max=1000.0), 

     Node.Socket('Variation', val=0.5, min=0.0, 

            max=1.0)]

    sockets.output = [Node.Socket('Distance', 

             val=0.5, min=0.0, max=1.0)]

Вычисления отраженного цвета начинается с получением списка всех ламп на сцене, так как мы хотим вычислить угол падающих световых лучей. Сейчас, мы принимаем во внимание вклад только первой лампы, которую мы нашли. Тем не менее, более полная реализация должна рассматривать все лампы, и может быть, даже их цвет. Для наших вычислений мы должны убедиться, что нормаль поверхности N и вектор падения света L находятся в одном и том же пространстве. Так как предоставляемая нормаль поверхности будет в пространстве камеры, мы должны трансформировать этот вектор матрицей преобразования камеры, как мы это делали для нашего наклоно-зависимого шейдера (выделено в следующем куске кода):

  def __call__(self):


    P = vec(self.input.Coords)

    scn=Scene.GetCurrent()

    lamps = [ob for ob in scn.objects if 

        ob.type == 'Lamp']


    lamp = lamps[0]


    cam=scn.objects.camera

    rot=cam.getMatrix('worldspace').rotationPart(

                    ).resize4x4();

    N = vec(self.shi.surfaceNormal).normalize(

                    ).resize4D() * rot


    N = N.negate().resize3D()

    L = vec(lamp.getLocation('worldspace'))

    I = (P – L).normalize()

Затем, мы вычисляем угол между нормалью поверхности и вектором падения (VecT - псевдоним для функции Mathutils.angleBetweenVecs()), и используем этот угол падения, чтобы вычислить угол между нормалью поверхности внутри водяной плёнки, так как он определяет расстояние прохождения света. Мы используем закон Снелла для его вычисления, а для показателя преломления водной плёнки возьмём 1.31. Расчет расстояния после этого - вопрос простой тригонометрии (выделено ниже):

    angle = VecT(I,N)


    angle_in = pi*angle/180

    sin_in = sin(angle_in)

    sin_out = sin_in/1.31

    angle_out = asin(sin_out)


    thickness = self.input.Thickness + \

          self.input.Variation

    distance = 2.0 * (thickness / cos (angle_out))

Рассчитанное расстояние равняется длине волны цвета, который мы воспримем. Тем не менее, Блендер работает не с длинами волн, а с цветами RGB, так что нам всё еще нужно преобразовать эту длину волны в кортеж (R, G, B), который представляет тот же цвет. Это можно было бы сделать посредством применения некоей спектральной формулы (смотрите, например, здесь: http://www.philiplaven.com/p19.html), но, может быть, будет даже более универсальным вариантом масштабировать это рассчитанное расстояние, и использовать его как вход для цветовой полосы (color band). Таким образом мы можем воспроизвести не-физически точную радужность (если захотим):

    self.output.Distance = distance

Чтобы использовать этот Pynode, нужно иметь в виду некоторые моменты. Сначала, убедитесь, что рассчитанный цвет влияет только на цвет specular материала мыльного пузыря, в противном случае всё покажется вымытым.

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

Наконец, сделайте материал мыльной плёнки очень прозрачным, но с высокой отражательной способностью (specular). Экспериментируйте с величинами, чтобы добиваться точного эффекта, и примите во внимание настройку освещения. Пример, показанный на иллюстрации – пробный, чтобы получить некий результат в черно-белом представлении, и, следовательно, не реалистичен, но сеть в файле примера iridescence.blend настроена производить красочный приятный результат при рендере.



Использование color ramp и текстуры шума показано на предыдущем скриншоте, куда мы добавили несколько нодов деления, чтобы масштабировать наше расстояние в дипазон в пределах [0,1], который можно использовать как вход для color ramp:



Итог

В этой главе мы увидели, что отсутствие компилируемого шейдерного языка в Блендере не препятствует использованию в нём cпроектированных заказных узоров и шейдеров. Pynodes - встроенная часть нодовой системы Блендера, и мы увидели как использовать их для создания эффектов, от простых цветных узоров до довольно сложных анимированных волн. В частности, мы узнали:

• Как писать Pynodes, которые создают простые цветные узоры

• Как писать Pynodes, которые производят узоры с нормалями

• Как писать анимированные Pynodes

• Как писать материалы, зависимые от высоты и наклона

• Как создавать шейдеры, которые реагируют на угол падающего света

В следующей главе мы посмотрим на автоматизацию процесса рендера в целом.


Загрузка...