steps3D - Tutorials - Flow textures

Flow Textures

Иногда возникает задача анимировать каким-либо более или менее правдоподобным образом заданную текстуру. Например, подобная задача часто возникает при рендеринге огня или воды. В этой статье будет рассмотрен один способ подобной анимации на основе так называемых flow textures (текстур движения).

Понятно, что мы должны некоторым образом менять текстурные координаты по которым мы читаем значения из текстуры. давайте заведем вспомогательную текстуру движения (flow texture), которая будет задавать смещение текстурных координат со временем (в каналах R и G). На следующем рисунке приведены модельная текстура, которую мы будем анимировать и текстура движения.

Рис 1. Базовая (анимируемая) текстура и текстура движения

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

-- vertex

#version 330 core

layout(location = 0) in vec4 pos;

out vec2 tex;

void main(void)
{
    tex         = vec2 ( 1, -1 ) * pos.zw;
    gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}

-- fragment

#version 330 core

uniform sampler2D image;
uniform sampler2D flowMap;
uniform float     time;
in  vec2 tex;
out vec4 color;

vec2 flowUV ( in vec2 uv, in vec2 flowVec, float time )
{
    return uv - flowVec * time;
}

void main (void)
{
    vec2    flowVec = texture ( flowMap, tex ).rg * 2 - vec2 ( 1.0 );
    vec2    uv      = flowUV  ( tex, flowVec, time );
    
    color = texture ( image, uv );
}

Если запустить соответствующую программу (flow-texture-1.py) то мы увидим анимация текстуры. Однако со временем мы будем получать очень сильные искажения , поэтому желательно как-то ограничить изменение текстурных координат со временем. Самый простой способ получения этого будет взять дробную часть от времени и ее умножать на значение из текстуры движения. Кроме того давайте сразу умножим главную(анимируемую) текстуру на на специальную периодическую функцию от времени (seesaw), соответствующий шейдер (flow-2.glsl) приводится ниже.

-- vertex

#version 330 core

layout(location = 0) in vec4 pos;

out vec2 tex;

void main(void)
{
    tex         = vec2 ( 1, -1 ) * pos.zw;
    gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}

-- fragment

#version 330 core

uniform sampler2D image;
uniform sampler2D flowMap;
uniform float     time;
in  vec2 tex;
out vec4 color;

float seesaw ( in float x )
{
    return 1 - abs ( 1 - 2 * x );
}

vec3 flowUV ( in vec2 uv, in vec2 flowVec, float time )
{
    float progress = mod ( time, 1.0 );
    return vec3 ( uv - flowVec * progress, seesaw ( progress ) );
}

void main (void)
{
    vec2    flowVec = texture ( flowMap, tex ).rg * 2 - vec2 ( 1.0 );
    vec3    uv      = flowUV  ( tex, flowVec, time );
    
    color = texture ( image, uv.xy ) * uv.z;
}

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

Рис 2. Текстура движения с шумом

-- vertex

#version 330 core

layout(location = 0) in vec4 pos;

out vec2 tex;

void main(void)
{
    tex         = vec2 ( 1, -1 ) * pos.zw;
    gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}

-- fragment

#version 330 core

uniform sampler2D image;
uniform sampler2D flowMap;
uniform float     time;
in  vec2 tex;
out vec4 color;

float seesaw ( in float x )
{
    return 1 - abs ( 1 - 2 * x );
}

vec3 flowUV ( in vec2 uv, in vec2 flowVec, float time )
{
    float progress = mod ( time, 1.0 );
    return vec3 ( uv - flowVec * progress, seesaw ( progress ) );
}

void main (void)
{
    vec4    flow    = texture ( flowMap, tex );
    float    noise   = flow.a;
    vec2    flowVec = flow.rg * 2 - vec2 ( 1.0 );
    vec3    uv      = flowUV  ( tex, flowVec, time + noise );
    
    color = texture ( image, uv.xy ) * uv.z;
}

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

Рис. 3 Веса для сложения двух анимаций

-- vertex

#version 330 core

layout(location = 0) in vec4 pos;

out vec2 tex;

void main(void)
{
    tex         = vec2 ( 1, -1 ) * pos.zw;
    gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}

-- fragment

#version 330 core

uniform sampler2D image;
uniform sampler2D flowMap;
uniform float     time;
in  vec2 tex;
out vec4 color;

float seesaw ( in float x )
{
    return 1 - abs ( 1 - 2 * x );
}

vec3 flowUV ( in vec2 uv, in vec2 flowVec, float time, bool flowB )
{
    float    phaseOffset = flowB ? 0.5 : 1;
    float    progress = mod ( time + phaseOffset, 1.0 );

    return vec3 ( uv - flowVec * progress + vec2 ( phaseOffset ), seesaw ( progress ) );
}

void main (void)
{
    vec4    flow    = texture ( flowMap, tex );
    float    noise   = flow.a;
    vec2    flowVec = flow.rg * 2 - vec2 ( 1.0 );
    vec3    uvA      = flowUV  ( tex, flowVec, time + noise, false );
    vec3    uvB      = flowUV  ( tex, flowVec, time + noise, true  );
    
    color = texture ( image, uvA.xy ) * uvA.z + texture ( image, uvB.xy ) * uvB.z;
}

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

-- vertex

#version 330 core

layout(location = 0) in vec4 pos;

out vec2 tex;

void main(void)
{
    tex         = vec2 ( 1, -1 ) * pos.zw;
    gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}

-- fragment

#version 330 core

uniform sampler2D image;
uniform sampler2D flowMap;
uniform float     time;

in  vec2 tex;
out vec4 color;

const vec2    jump         = vec2    ( 0.24, 0.20833 );
const float     flowStrength = 1;
const float    tiling       = 2;

float seesaw ( in float x )
{
    return 1 - abs ( 1 - 2 * x );
}

vec3 flowUV ( in vec2 uv, in vec2 flowVec, in vec2 jump, float tiling, float time, bool flowB )
{
    float    phaseOffset = flowB ? 0.5 : 1;
    float    progress = mod ( time + phaseOffset, 1.0 );

    return vec3 ( (uv - flowVec * progress) * tiling + vec2 ( phaseOffset ) + (time - progress) * jump, seesaw ( progress ) );
}

void main (void)
{
    vec4    flow     = texture ( flowMap, tex );
    float    noise    = flow.a;
    vec2    flowVec  = (flow.rg * 2 - vec2 ( 1.0 )) * flowStrength;
    vec3    uvA      = flowUV  ( tex, flowVec, jump, tiling, time + noise, false );
    vec3    uvB      = flowUV  ( tex, flowVec, jump, tiling, time + noise, true  );
    
    color = texture ( image, uvA.xy ) * uvA.z + texture ( image, uvB.xy ) * uvB.z;
}

Ниже приводится исходный код на python для всех примеров (отличие между ними заключаются в используемом шейдере - от flow-1.glsl flow-5.glsl)

import math
import glm
import numpy
from OpenGL.GL import *
import Window
import Program
import Texture
import Mesh
import Framebuffer
import dds
import Screen

class   MyWindow ( Window.RotationWindow ):
    def __init__ ( self, w, h, t ):
        super().__init__ ( w, h, t )
        self.texture = Texture.Texture ( "uv.jpg" )
        self.flow    = Texture.Texture ( "flowmap-2.png" )
        self.mesh    = Screen.Screen ()
        self.shader  = Program.Program ( glsl = "flow-5.glsl" )
        self.shader.use         ()
        self.shader.setTexture  ( "image",   0 )
        self.shader.setTexture  ( "flowMap", 1 )
        self.texture.bind ( 0 )
        self.flow.bind    ( 1 )

    def redisplay ( self ):
        glClearColor ( 0.5, 0.5, 0.5, 1.0 )
        glClear      ( GL_COLOR_BUFFER_BIT + GL_DEPTH_BUFFER_BIT )

        self.shader.use         ()
        self.shader.setUniformFloat ( "time", self.time () )
        self.mesh.render ()

def main():
    win = MyWindow ( 900, 900, "Flow texturing" )
    win.run ()

if __name__ == "__main__":
    main()

Этот пример (со всеми шейдерами, кодом на python и текстурами) можно скачать в репозитории на github.com - https://github.com/steps3d/graphics-book/tree/master/Code/python-code.