WebGL: Отбрасываем реалистичные тени прямо в браузере
medium

WebGL: Отбрасываем реалистичные тени прямо в браузере

Разбор сложного проекта, который показывает возможности современного веба

Мы начали разговор об освещении, тенях и рейтрейсинге в компьютере. В прошлый раз говорили о принципах рейтрейсинга — это когда компьютер моделирует то, как свет взаимодействует с трёхмерными объектами на разных поверхностях и при разных условиях. Такой подход позволяет получать реалистичные 3D-изображения.

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

Чтобы было понятнее, что тут происходит:

Мы попробуем смоделировать поведение света с помощью браузера и трёхмерного движка WebGL

Что делаем

Мы разберём уже готовый проект Александра Шульмана. Разбирать чужие проекты полезно, чтобы погрузиться в новую для себя отрасль. 

Основные шаги его программы: 

  1. Пишем служебные функции и подключаем движок WebGL.
  2. Создаём источник освещения и прописываем его параметры.
  3. Создаём слой с предметами в сцене.
  4. Поверх него создаём слой для теней.
  5. Рендерим сцену.
  6. Сдвигаем источник света и пересчитываем всё заново.

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

Подготовка

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

////by Ethan Alexander Shulman known as public_int_i 10100101010
//This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
//http://creativecommons.org/licenses/by-nc-sa/4.0/

// переменные для обработки шейдеров
var quadVB;
var shVertex;

// вспомогательные переменные
var dfTex,alTex;
var uniTime;

// переменная для времени старта
var startTime;

// переменные для доступа к движку
var cE,gl;

// высота и ширина сцены
var WIDTH = 640;
var HEIGHT = 480;
// масштаб
var DF_SCALE = 1.0;
// финальная высота и ширина сцены
var DF_WIDTH = WIDTH*DF_SCALE;
var DF_HEIGHT = HEIGHT*DF_SCALE;

Служебные функции

Чтобы упростить написание кода, Александр делает собственные математические функции на основе встроенных. При этом он использует два подхода — сокращение и упорядочивание.

Сокращение — это когда вместо вызова длинного Math.abs() для модуля числа можно вызвать просто abs() и получить тот же результат. Для этого он просто создаёт переменные (которые на самом деле — функции) и работает через них:

// формируем удобные вызовы функций
var abs = Math.abs;
var floor = Math.floor;
var sqrt = Math.sqrt;

Упорядочивание — это когда вместо однотипных функций, но с разными параметрами используются новые функции с удобными названиями. Например, чтобы узнать расстояние до точки, нужно извлечь корень из суммы произведений координат, но проще сделать отдельную функцию len():

// расстояние до точки с указанными координатами
function len(x,y) {
  return sqrt(x*x+y*y);
}
// расстояние до круга с указанными координатами
function circle(x,y,r) {
  return len(x,y)-r;
}

// расстояние до куба с указанными координатами
function cube(x,y,w,h,r) {
  return len(max(0.,abs(x)-w),
             max(0.,abs(y)-h))-r;
}

// возвращает минимальное значение из двух
function min(a,b) {
  return a<b?a:b;
}

// возвращает максимальное значение из двух
function max(a,b) {
  return a>b?a:b;
/ возвращает расстояние до указанного объекта
function distanceField(x,y) {
  var lx = x%.4-.2, ly = y%.4-.2;
  var fid = (floor(x/.4)+floor(y/.4)*12.)%144.;
  return min(cube(lx,ly,.014+(fid*8.213461)%1.*.1,.014+(fid*4.7234)%1.*.1,.001),
             min(circle(x-.5,y-.5,.2), cube(x-.1,y-.5,.01,.34,.01)));
}

Коротко про шейдеры

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

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

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

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

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

Получается, что вершинные и фрагментные шейдеры решают разные задачи: одни отвечают за геометрию трёхмерной модели, а другие — за то, как эта модель будет выглядеть.

Если коротко описать процесс формирования финальной картинки на экране из трёхмерной сцены, то она выглядит так:

  1. Описываем объекты.
  2. Создаём вершинные шейдеры, кладём в буфер и обрабатываем их.
  3. Создаём фрагментные шейдеры и тоже их обрабатываем через буфер.
  4. Объединяем всё и выводим итоговую картинку.

Готовим движок

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

Движок можно запустить в тот момент, когда страница полностью загрузилась. Для этого создаём функцию window.onload = function() {} и делаем такую предварительную работу:

// что делаем, когда страница загрузилась 
window.onload = function() {
  // получаем доступ к окну
  cE = document.getElementById("display");
  // устанавливаем свои высоту и ширину
  cE.width = WIDTH;
  cE.height = HEIGHT;
  // подключаем движок WbbGL
  gl = cE.getContext("webgl");
  // если не получилось
  if (!gl) {
    // пробуем использовать экспериментальную версию
    gl =cE.getContext("experimental-webgl");
    // если не сработало — выводим сообщение об ошибке
    if (!gl) alert("Failed to load WebGL");
  }
  
  // координаты вершин в углах сцены
  var quadVerts = new Float32Array([1.0,1.0,-1.0,1.0,1.0,-1.0,-1.0,-1.0]);
  // создаём буфер вершин
  quadVB = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER,quadVB);
  // координаты вершин в буфере
  gl.bufferData(gl.ARRAY_BUFFER,quadVerts,gl.STATIC_DRAW);
  // создаём вершинные шейдеры
  var vso = gl.createShader(gl.VERTEX_SHADER);
  // устанавливаем источник шейдеров и прописываем параметры
  gl.shaderSource(vso,"precision mediump float; varying vec2 uv; attribute vec2 vertex; void main(void) {uv=.5*vertex+vec2(.5);gl_Position=vec4(vertex,0.,1.);}");
  // компилируем шейдеры
  gl.compileShader(vso);
  // и сразу вызываем отладчик
  debugShader(vso);

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

// создаём фрагментные шейдеры
  var pso = gl.createShader(gl.FRAGMENT_SHADER);
  // устанавливаем источник и прописываем параметры
  // здесь же будет и движение 
  gl.shaderSource(pso,"\nprecision mediump float; varying vec2 uv; uniform sampler2D tex0,tex1; uniform float time;"+
                  
   "void main(void) {"+
   "float sh=1.,occl=1.;"+
   "vec2 lightPos = vec2(sin(time)*.5+.25,cos(time)*.4+.5);"+
   "vec2 lightDir = lightPos-uv;"+
   "float lightDist = length(lightDir); if (lightDist < .02) {gl_FragColor=vec4(1.,1.,0.,1.);return;}float sd=lightDist;"+
   "lightDir = normalize(lightDir);"+
   "vec2 rp = uv; float dst = texture2D(tex1,uv).x;"+
   "for (int i = 0; i < 44; i++) {"+               
   "lightDist -= dst; if (lightDist < 0.) break;"+
   "if (dst < .0004) {sh=0.;break;}"+
   "rp += dst*lightDir; dst=texture2D(tex1,rp).x;"+
   "occl -= max(0.,.02-dst)*8.;"+
   "}"+
   "vec3 dif = texture2D(tex0,uv).xyz; dif = .2*dif+max(0.,1.-sd/.8)*sh*max(0.,occl)*.8*dif;"+
   "gl_FragColor=vec4(dif,1.);}");
                  
  // компилируем фрагментные шейдеры 
  gl.compileShader(pso);
  // сразу вызываем отладчик шейдеров
  debugShader(pso);

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

// создаём объект программы шейдеров
  var spr = gl.createProgram();
  // прикрепляем к ней шейдеры
  gl.attachShader(spr,vso);
  gl.attachShader(spr,pso);
  // связываем программу с webgl
  gl.linkProgram(spr);
  // указываем, какую программу будем использовать
  gl.useProgram(spr);

  // получаем текущее время
  startTime = Date.now();
  // получаем текущее положение шейдеров
  uniTime = gl.getUniformLocation(spr,"time");
  shVertex= gl.getAttribLocation(spr,"vertex");
  // подключаем атрибут для исползования
  gl.enableVertexAttribArray(shVertex);
  
  // создаём текстуры
  dfTex = gl.createTexture();
  alTex = gl.createTexture();

Рисуем слои

Теперь нам нужно научить движок отрисовывать два слоя:

  1. Слой с непрозрачными объектами.
  2. Слой освещения, где будут двигаться тени.

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

Логика первого слоя работает так: 

  1. Перебираем все пиксели на картинке.
  2. Для каждого пикселя смотрим, попадает ли он внутрь объекта или нет.
  3. Если попадает — становится тёмным (это объект), если не попадает — светлым (это фон).

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

// формируем первый слой — фон и фигуры

  // создаём холст для отрисовки всего
  var bc =document.createElement("canvas");
  // устанавливаем высоту и ширину
  bc.width = WIDTH;
  bc.height = HEIGHT;
  // получаем доступ к холсту
  var b2d = bc.getContext("2d"),u,v;
  // перебираем все пиксели на холсте
  for (var x = 0; x < WIDTH; x++) {
    u = x/WIDTH;
    for (var y = 0; y < HEIGHT ; y++) {
      v = y/HEIGHT;
      // получаем расстояние до объекта
      var d = distanceField(u,v);
      // если дистанция меньше погрешности
      if (d < 0.0008) {
        // это фигура
      b2d.fillStyle = "#808080";
      } else {
        // это пустое пространство
      b2d.fillStyle = "white";
      }
      // красим пиксель в свой цвет
      b2d.fillRect(x,y,1,1);
    }
  }
  // указываем активную текстуру и делаем её основной
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D,alTex);
  // по правилам все текстуры квадратные, но не все объекты можно разбить на целое число квадратных элементов
  // заполняем пиксели до краёв текстур, даже если они не помещаются в квадрат
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bc);
  gl.uniform1i(gl.getUniformLocation(spr,"tex0"),0);

Второй слой — это тени, и здесь у нас меняется логика в цикле. Если раньше мы просто смотрели, попали или не попали в объект, то теперь считаем расстояние от источника света до всех остальных пикселей. Чем ближе — тем светлее. Но если на пути встретилась преграда — туда свет не идёт, поэтому там тоже будет тень:

// делаем то же самое для второго слоя — на нём будут тени

  var dc = document.createElement("canvas");
  dc.width = DF_WIDTH;
  dc.height = DF_HEIGHT;
  var d2d = dc.getContext("2d"); 
  var floor = Math.floor;
  for (var x = 0; x < DF_WIDTH; x++) {
    u = x/DF_WIDTH;
    for (var y = 0; y < DF_HEIGHT ; y++) {
      v = y/DF_HEIGHT;
      // считаем расстояние от источника света
      var d = distanceField(u,v)*255;
      d = floor(d);
      // и заливаем картинку тенью, чем дальше — тем темнее
      d2d.fillStyle = "rgb("+d+","+d+","+d+")";
      d2d.fillRect(x,y,1,1);
    }
  }
  
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D,dfTex);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, dc);
  gl.uniform1i(gl.getUniformLocation(spr,"tex1"),1);

Последнее, что нам осталось тут сделать, — сразу же отрисовать то, что у нас получилось к этому моменту:

// если сейчас отрисовка закончилась — запускаем новую
if (!window.requestAnimationFrame) window.requestAnimationFrame=window.setTimeout;
// рендерим готовую картинку
render();
// закончилась функция window.onload
}

Запускаем сцену

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

// функция рендера
function render() {
  // очищаем сцену
  gl.clear(gl.COLOR_BUFFER_BIT);
  // получаем новое положение шейдеров
  gl.uniform1f(uniTime,(Date.now()-startTime)/2000.0);
  // отправляем в буфер
  gl.bindBuffer(gl.ARRAY_BUFFER,quadVB);
  // передаём координаты
  gl.vertexAttribPointer(shVertex, 2, gl.FLOAT, false, 0, 0);
  // отрисовываем треугольники
  gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
  // повторяем бесконечное количество раз
  setTimeout(render);
}

Посмотреть на игру света и тени на странице проекта

////by Ethan Alexander Shulman known as public_int_i 10100101010
//This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
//http://creativecommons.org/licenses/by-nc-sa/4.0/

// переменные для обработки шейдеров
var quadVB;
var shVertex;

// вспомогательные переменные
var dfTex,alTex;
var uniTime;

// переменные для доступа к движку
var cE,gl;

// высота и ширина сцены
var WIDTH = 640;
var HEIGHT = 480;
// масштаб
var DF_SCALE = 1.0;
// финальная высота и ширина сцены
var DF_WIDTH = WIDTH*DF_SCALE;
var DF_HEIGHT = HEIGHT*DF_SCALE;

// отладка шейдеров
function debugShader(s,t) {
  // если шейдер не скомпилировался нормально
   if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
        // возвращаем -1
        return -1;
   }
}

// формируем удобные вызовы функций
var abs = Math.abs;
var floor = Math.floor;
var sqrt = Math.sqrt;

// расстояние до точки с указанными координатами
function len(x,y) {
  return sqrt(x*x+y*y);
}
// расстояние до круга с указанными координатами
function circle(x,y,r) {
  return len(x,y)-r;
}

// расстояние до куба с указанными координатами
function cube(x,y,w,h,r) {
  return len(max(0.,abs(x)-w),
             max(0.,abs(y)-h))-r;
}

// возвращает минимальное значение из двух
function min(a,b) {
  return a<b?a:b;
}

// возвращает максимальное значение из двух
function max(a,b) {
  return a>b?a:b;
}

// возвращает расстояние до указанного объекта
function distanceField(x,y) {
  var lx = x%.4-.2, ly = y%.4-.2;
  var fid = (floor(x/.4)+floor(y/.4)*12.)%144.;
  return min(cube(lx,ly,.014+(fid*8.213461)%1.*.1,.014+(fid*4.7234)%1.*.1,.001),
             min(circle(x-.5,y-.5,.2), cube(x-.1,y-.5,.01,.34,.01)));
}

// что делаем, когда страница загрузилась 
window.onload = function() {
  // получаем доступ к окну
  cE = document.getElementById("display");
  // устанавливаем свои высоту и ширину
  cE.width = WIDTH;
  cE.height = HEIGHT;
  // подключаем движок WbbGL
  gl = cE.getContext("webgl");
  // если не получилось
  if (!gl) {
    // пробуем использовать экспериментальную версию
    gl =cE.getContext("experimental-webgl");
    // если не сработало — выводим сообщение об ошибке
    if (!gl) alert("Failed to load WebGL");
  }
  
  // координаты вершин в углах сцены
  var quadVerts = new Float32Array([1.0,1.0,-1.0,1.0,1.0,-1.0,-1.0,-1.0]);
  // создаём буфер вершин
  quadVB = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER,quadVB);
  // координаты вершин в буфере
  gl.bufferData(gl.ARRAY_BUFFER,quadVerts,gl.STATIC_DRAW);
  // создаём вершинные шейдеры
  var vso = gl.createShader(gl.VERTEX_SHADER);
  // устанавливаем источник шейдеров и прописываем параметры
  gl.shaderSource(vso,"precision mediump float; varying vec2 uv; attribute vec2 vertex; void main(void) {uv=.5*vertex+vec2(.5);gl_Position=vec4(vertex,0.,1.);}");
  // компилируем шейдеры
  gl.compileShader(vso);
  // и сразу вызываем отладчик
  debugShader(vso);
  
  // создаём фрагментные шейдеры
  var pso = gl.createShader(gl.FRAGMENT_SHADER);
  // устанавливаем источник и прописываем параметры
  // здесь же будет и движение 
  gl.shaderSource(pso,"\nprecision mediump float; varying vec2 uv; uniform sampler2D tex0,tex1; uniform float time;"+
                  
   "void main(void) {"+
   "float sh=1.,occl=1.;"+
   "vec2 lightPos = vec2(sin(time)*.5+.25,cos(time)*.4+.5);"+
   "vec2 lightDir = lightPos-uv;"+
   "float lightDist = length(lightDir); if (lightDist < .02) {gl_FragColor=vec4(1.,1.,0.,1.);return;}float sd=lightDist;"+
   "lightDir = normalize(lightDir);"+
   "vec2 rp = uv; float dst = texture2D(tex1,uv).x;"+
   "for (int i = 0; i < 44; i++) {"+               
   "lightDist -= dst; if (lightDist < 0.) break;"+
   "if (dst < .0004) {sh=0.;break;}"+
   "rp += dst*lightDir; dst=texture2D(tex1,rp).x;"+
   "occl -= max(0.,.02-dst)*8.;"+
   "}"+
   "vec3 dif = texture2D(tex0,uv).xyz; dif = .2*dif+max(0.,1.-sd/.8)*sh*max(0.,occl)*.8*dif;"+
   "gl_FragColor=vec4(dif,1.);}");
                  
  // компилируем фрагментные шейдеры 
  gl.compileShader(pso);
  // сразу вызываем отладчик шейдеров
  debugShader(pso);

  // создаём объект программы шейдеров
  var spr = gl.createProgram();
  // прикрепляем к ней шейдеры
  gl.attachShader(spr,vso);
  gl.attachShader(spr,pso);
  // связываем программу с webgl
  gl.linkProgram(spr);
  // указываем, какую программу будем использовать
  gl.useProgram(spr);

  // получаем текущее время
  startTime = Date.now();
  // получаем текущее положение шейдеров
  uniTime = gl.getUniformLocation(spr,"time");
  shVertex= gl.getAttribLocation(spr,"vertex");
  // подключаем атрибут для исползования
  gl.enableVertexAttribArray(shVertex);
  
  // создаём текстуры
  dfTex = gl.createTexture();
  alTex = gl.createTexture();
  
// формируем первый слой — фон и фигуры

  // создаём холст для отрисовки всего
  var bc =document.createElement("canvas");
  // устанавливаем высоту и ширину
  bc.width = WIDTH;
  bc.height = HEIGHT;
  // получаем доступ к холсту
  var b2d = bc.getContext("2d"),u,v;
  // перебираем все пиксели на холсте
  for (var x = 0; x < WIDTH; x++) {
    u = x/WIDTH;
    for (var y = 0; y < HEIGHT ; y++) {
      v = y/HEIGHT;
      // получаем расстояние до объекта
      var d = distanceField(u,v);
      // если дистанция меньше погрешности
      if (d < 0.0008) {
        // это фигура
      b2d.fillStyle = "#808080";
      } else {
        // это пустое пространство
      b2d.fillStyle = "white";
      }
      // красим пиксель в свой цвет
      b2d.fillRect(x,y,1,1);
    }
  }
  // указываем активную текстуру и делаем её основной
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D,alTex);
  // по правилам все текстуры квадратные, но не все объекты можно разбить на целое число квадратных элементов
  // заполняем пиксели до краёв текстур, даже если они не помещаются в квадрат
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, bc);
  gl.uniform1i(gl.getUniformLocation(spr,"tex0"),0);
  
// делаем то же самое для второго слоя — на нём будут тени

  var dc = document.createElement("canvas");
  dc.width = DF_WIDTH;
  dc.height = DF_HEIGHT;
  var d2d = dc.getContext("2d"); 
  var floor = Math.floor;
  for (var x = 0; x < DF_WIDTH; x++) {
    u = x/DF_WIDTH;
    for (var y = 0; y < DF_HEIGHT ; y++) {
      v = y/DF_HEIGHT;
      // считаем расстояние от источника света
      var d = distanceField(u,v)*255;
      d = floor(d);
      // и заливаем картинку тенью, чем дальше — тем темнее
      d2d.fillStyle = "rgb("+d+","+d+","+d+")";
      d2d.fillRect(x,y,1,1);
    }
  }
  
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D,dfTex);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  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.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, dc);
  gl.uniform1i(gl.getUniformLocation(spr,"tex1"),1);
  
  // если сейчас отрисовка закончилась — запускаем новую
  if (!window.requestAnimationFrame) window.requestAnimationFrame=window.setTimeout;
  // рендерим готовую картинку
  render();
}
// переменная для времени старта
var startTime;

// функция рендера
function render() {
  // очищаем сцену
  gl.clear(gl.COLOR_BUFFER_BIT);
  // получаем новое положение шейдеров
  gl.uniform1f(uniTime,(Date.now()-startTime)/2000.0);
  // отправляем в буфер
  gl.bindBuffer(gl.ARRAY_BUFFER,quadVB);
  // передаём координаты
  gl.vertexAttribPointer(shVertex, 2, gl.FLOAT, false, 0, 0);
  // отрисовываем треугольники
  gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
  // повторяем бесконечное количество раз
  setTimeout(render);
}

Вы фронтендер? Гляньте сюда ↓
У нас есть курсы по прокачке навыков до уровня middle и далее. Начать можно бесплатно. Посмотрите, интересно ли вам это.
Начать бесплатно
Вы фронтендер? Гляньте сюда ↓ Вы фронтендер? Гляньте сюда ↓ Вы фронтендер? Гляньте сюда ↓ Вы фронтендер? Гляньте сюда ↓
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium