Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
Иногда возникает задача анимировать каким-либо более или менее правдоподобным образом заданную текстуру. Например, подобная задача часто возникает при рендеринге огня или воды. В этой статье будет рассмотрен один способ подобной анимации на основе так называемых 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.