4

Подскажите, есть ли легкий способ создать эффекта миража(марева) на технологиях canvas / svg для изображения. В приведенном примере используется перемещение текстур, можно ли проще, к примеру для анимации эффекта огня?

Alexandr_TT
  • 110,146
  • 23
  • 114
  • 384
  • Как бы описание эффекта есть : https://tympanus.net/codrops/2016/05/03/animated-heat-distortion-effects-webgl/ – Резидент Казахстана Feb 07 '19 at 05:02
  • Это я читал, только не смог толком разобраться. У меня изображение несколько другое и я не смог создать правильную маску перекрытия. Потому-то и ищу решение попроще. – BlackStar1991 Feb 07 '19 at 06:06
  • webgl - я думаю здесь ни кто объяснять не будет - это на самом деле сложное для понимания, это и математика и отличное знание js и написание шейдера это прежде всего взаимодействие на графический процессор ... – Резидент Казахстана Feb 07 '19 at 06:13
  • хотя вопрос сам по себе достоин ответа ... подробного ответа ...жаль нету опции Подписаться – Резидент Казахстана Feb 07 '19 at 08:13
  • Дайте знать если Вас еще интересует webgl реализация такого эффекта – Stranger in the Q Feb 28 '19 at 20:07

2 Answers2

6

Выбираем картинку с объемными объектами на дальнем плане, чтобы эффект миража лучше проявился.

Применение фильтров сильно нагружает видеокарту, поэтому при реальном применении этого эффекта старайтесь уменьшить размеры зоны действия фильтра.

введите сюда описание изображения

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

За основу создания эффекта миража в фильтре отвечают несколько параметров, но основной параметр это baseFrequency

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

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

scale="6" - увеличивает размер волн

seed="53" - отвечает за слоистость структуры

frames += 0.3 - увеличение прироста фраймов, увеличивает скорость волновых эффектов

var img = document.querySelector("#displacementFilter feTurbulence");
var frames = 0;
var rad = Math.PI / 180;

function AnimateBaseFrequency() { //baseFrequency="0.01 .1" bfx = 0.01; bfy = 0.1; frames += 0.8 bfx += 0.01 * Math.cos(frames * rad); bfy += 0.01 * Math.sin(frames * rad);

bf = bfx.toString() + ' ' + bfy.toString(); img.setAttributeNS(null, 'baseFrequency', bf);

window.requestAnimationFrame(AnimateBaseFrequency); }

window.requestAnimationFrame(AnimateBaseFrequency);

.container {
width:50%;
height:50%;
}
<div class="container">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink"
        viewBox="0 0 673 455" preserveAspectRatio="none" >  

 <filter id="displacementFilter">
    <feTurbulence type="turbulence" baseFrequency="0.1 .1"
        numOctaves="1" result="turbulence" seed="53"/>
    <feDisplacementMap in2="turbulence" in="SourceGraphic"
        scale="6" xChannelSelector="R" yChannelSelector="B"/>
  </filter>    

<image id="blueMoon"  y="90" xlink:href="https://i.stack.imgur.com/ke2Ih.png" width="100%" height="100%" />

   <use xlink:href ="#blueMoon"   transform="translate(0, 0) scale(1 1) " filter="url(#displacementFilter)"/>
</svg>   
</div> 

Пламя

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

 var img = document.querySelector("#displacementFilter feTurbulence");
var frames = 0;
var rad = Math.PI / 180;

function AnimateBaseFrequency() { //baseFrequency="0.01 .1" bfx = 0.1; bfy = 0.1; frames += .4 bfx += 0.05 * Math.cos(frames * rad); bfy += 0.1 * Math.sin(frames * rad);

bf = bfx.toString() + ' ' + bfy.toString(); img.setAttributeNS(null, 'baseFrequency', bf);

window.requestAnimationFrame(AnimateBaseFrequency); }

window.requestAnimationFrame(AnimateBaseFrequency);

.container {
width:50%;
height:50%;
}
<div class="container">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink"
        viewBox="0 0 670 526" preserveAspectRatio="none" >  

 <filter id="displacementFilter">
    <feTurbulence type="turbulence" baseFrequency="0.01 0.1"
        numOctaves="1" result="turbulence" seed="3"/>
    <feDisplacementMap in2="turbulence" in="SourceGraphic"
        scale="10" xChannelSelector="R" yChannelSelector="B"/>
  </filter>    

<image id="blueMoon"  y="90" xlink:href="https://i.stack.imgur.com/Pqhln.jpg"  width="80%" height="100%"  />

   <use xlink:href ="#blueMoon"    filter="url(#displacementFilter)"/>
</svg>   
</div> 

Огонь и вода

При обработке изображений необходимо уменьшить их размеры в любом растровом редакторе. Лучше в фотошопе, у которого есть недокументированная возможность уменьшать поэтапно на 10% размеры изображения без заметной потери качества. таким образом можно получить из изображения весом 2.2 Mb картинку весом 100kb В шапке svg файла необходимо выставить размер viewBox с атрибутами равными размеру используемой картинки. Например в этом примере - viewBox="0 0 670 526"

var img = document.querySelector("#displacementFilter feTurbulence");
var frames = 0;
var rad = Math.PI / 180;

function AnimateBaseFrequency() { //baseFrequency="0.01 .1" bfx = 0.1; bfy = 0.1; frames += .31 bfx += 0.1 * Math.cos(frames * rad); bfy += 0.1 * Math.sin(frames * rad);

bf = bfx.toString() + ' ' + bfy.toString(); img.setAttributeNS(null, 'baseFrequency', bf);

window.requestAnimationFrame(AnimateBaseFrequency); }

window.requestAnimationFrame(AnimateBaseFrequency);

.container {
width:50%;
height:50%;
<div class="container">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" 
    xmlns:xlink="http://www.w3.org/1999/xlink"
        viewBox="0 0 680 409" preserveAspectRatio="none" >  

 <filter id="displacementFilter">
    <feTurbulence type="turbulence" baseFrequency="0.1 .1"
        numOctaves="1" result="turbulence" seed="53"/>
    <feDisplacementMap in2="turbulence" in="SourceGraphic"
        scale="5" xChannelSelector="R" yChannelSelector="B"/>
  </filter>    

<image id="blueMoon"  y="90" xlink:href="https://i.stack.imgur.com/7ptDK.png" width="100%" height="100%" />

   <use xlink:href ="#blueMoon"     filter="url(#displacementFilter)"/>
</svg>   
</div>

Update

Во всех примерах используется requestAnimationFrame

Поддержка браузерами на 21.11.2021

введите сюда описание изображения

Caniuse.com

Alexandr_TT
  • 110,146
  • 23
  • 114
  • 384
  • 1
    Применение фильтров сильно нагружает видеокарту, поэтому при реальном применении этого эффекта старайтесь уменьшить размеры зоны действия фильтра. – Alexandr_TT Feb 07 '19 at 08:59
  • я лично хотел бы увидеть ответ по WebGL так как эта технология мощнее – Резидент Казахстана Feb 07 '19 at 09:15
  • 1
    Хороший ответ и ссылка замечательная! Вот почитай https://html5.by/blog/what-is-requestanimationframe/ как уменьшить количество обновлений для requestAnimationFrame – Bharatha Feb 08 '19 at 21:55
  • Да и кстати - ноут у меня так себе но если я открою пример с SVG у себя на весь экран то начинает шуметь куллер а вот если пример с tympanus то куллер молчит - что говорит об оптимизации процессора в коде WebGL что как раз не возможно сделать в SVG – Резидент Казахстана Feb 09 '19 at 04:34
2

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

Алгоритм:

  // запоминаем текущую текстурную координату
vec2 p_d = uv; 
  // прибавляем dt (сколько прошло времени с прошлого кадра)
p_d.y += t * 0.1; 
  // берем для полученного значения текстурных координат сэмпл 2d шума 
  // (тут могла быть текстура вместо вызова математического метода, и это будет быстрее)
vec2 offset = vec2(noise(p_d * 22.));     
  // плавно уменьшаем это значения до 0 (0 сверху)
offset *= uv.y * 0.01; 
  // берем цвет оригинальным координатам + смещение
return sample(uv.xy + offset); 

let t = new Date().getTime();
let url = "https://webgl2fundamentals.org/webgl/resources/images/computer-history-museum/pos-z.jpg";

let filter = webglFilter(url, `

uniform float t;

float rand(vec2 n) { return fract(sin(dot(n, vec2(12.9898, 4.1414))) * 43758.5453); }

float noise(vec2 p){ vec2 ip = floor(p); vec2 u = fract(p); u = uu(3.0-2.0*u);

float res = mix(
  mix(rand(ip),rand(ip+vec2(1.0,0.0)),u.x),
  mix(rand(ip+vec2(0.0,1.0)),rand(ip+vec2(1.0,1.0)),u.x),u.y);
return res*res;

}

vec4 frag (vec2 uv) { vec2 p_d = uv; p_d.y += t * 0.1; vec2 offset = vec2(noise(p_d22.)); offset = uv.y * 0.01; return sample(uv.xy + offset); } `);

filter.ready = function() { let c = filter.canvas; document.body.append(c); animate(); }

function animate() { filter.uniform('1f', 't', (new Date().getTime() - t)/1000) .apply(); requestAnimationFrame(animate); }

<script>
function webglFilter(url, fragCode) {

  let canvas = document.createElement('canvas');
  let pid, gl = canvas.getContext('webgl') 
        || canvas.getContext('experimental-webgl');

  let loader = new Image();
  loader.crossOrigin = "anonymous";
  loader.src = url;
  loader.onload = function() { 

    canvas.width = loader.width;
    canvas.height = loader.height;

    pid = gl.createProgram();

    shader(`
      attribute vec2 coords;
        void main(void) {
        gl_Position = vec4(coords.xy, 0.0, 1.0);
      }
    `, gl.VERTEX_SHADER);

    shader(`
      precision highp float;
      uniform sampler2D texture;

      vec4 sample(vec2 uv) {
          return texture2D(texture, uv);
      }

      ${fragCode}

      void main(void) {
          gl_FragColor = frag(vec2( 
            gl_FragCoord.x / ${canvas.width}.,  
            1. - gl_FragCoord.y / ${canvas.height}. 
          ));
      }
    `, gl.FRAGMENT_SHADER);

    gl.linkProgram(pid);
    gl.useProgram(pid);

    let array = new Float32Array([-1,  3, -1, -1, 3, -1]);
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW);

    let al = gl.getAttribLocation(pid, "coords");
    gl.vertexAttribPointer(al, 2, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(al);

    let texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, loader);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

    var textureLocation = gl.getUniformLocation(pid, "texture");
    gl.uniform1i(textureLocation, 0);

    filter.ready && filter.ready();
    filter.apply();

    function shader(src, type) {
      let sid = gl.createShader(type);
      gl.shaderSource(sid, src);
      gl.compileShader(sid);
      var message = gl.getShaderInfoLog(sid);
      gl.attachShader(pid, sid);
      if (message.length > 0) {
        console.log(src.split('\n').map(function (str, i) {
          return ("" + (1 + i)).padStart(4, "0") + ": " + str
        }).join('\n'));
        throw message;
      }
    }
  }

  let filter = {

    canvas: canvas,

    ready: null,

    uniform: function(type, name, v1, v2, v3, v4) {
      if (!pid)
        throw new Error('program not ready');
      var ul = gl.getUniformLocation(pid, name);
      gl['uniform' + type](ul, v1, v2, v3, v4);
      return filter;
    },

    apply: function() {
      if (!pid)
        throw new Error('program not ready');
      gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
      gl.clearColor(0, 0, 0, 0);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      return filter;
    }
  }

  return filter;
}
</script>
<style>
body{
margin:0;

}
</style>