понедельник, 9 февраля 2015 г.

О размещении исходного кода шейдеров

В своей книге К. Мацуда размещает исходный код шейдеров непосредственно в исходном коде js-файлов. Оформленный таким образом код шейдеров неудобно читать. В данной заметке рассматривается так же два дополнительных способа хранения исходного кода шейдеров и обращения к ним из кода JavaScript.

Вариант 1 (худший): размещение кода шейдеров непосредственно в коде JavaScript.
Итак, автор книги в своих начальных примерах определяет код шейдеров прямо в коде JavaScript следующим образом:

// Vertex shader programvar VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  gl_PointSize = 10.0;\n' +
  '}\n';

Такой способ успешно работает во всех современных популярных браузерах. Я проверял в IE 11, Firefox 35.0.1, Google Chrome 40.0.2214 и Opera 27.0. В этом случае код шейдера неудобно читать, а в случае необходимости его правки - придётся повторно делать это во всех js-файлах, где данный код фигурирует.

Вариант 2 (не самый лучший): размещение кода шейдеров в элементе script файла html.
Как вариант, можно было бы разместить код шейдеров в качестве контента элемента script в составе исходного HTML файла:


<script type="glsl" id="VSHADER_SOURCE">
  attribute vec4 a_position;
  float size = 10.0;
  void main(){
    gl_Position = a_position;
    gl_PointSize = size;
  }
</script>
<script type="glsl" id="FSHADER_SOURCE">
  void main(){
    gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0);
  }
</script>

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

var vsh_source = document.getElementById('VSHADER_SOURCE').textContent;
var fsh_source = document.getElementById('FSHADER_SOURCE').textContent;

UPD 
Обратите внимание, что в коде используется textContent вместо innerText. Если использовать innerText, то код будет корректно работать только в браузерах Google Chrome [40.0.2214] и Opera [27.0], но не будет работать в IE [11] и Firefox [35.0.1]. В квадратных скобках указаны версии, для которых я проверял работу кода.

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

По умолчанию, объект XMLHttpRequest запрещает загрузку шейдеров из внешних файлов, когда адрес текущей html-страницы, указанной в адресной строке браузера, начинается c "file:///". Однако он не запрещает делать это, если адрес начинается с "http://localhost:[Номер порта]/".

Т.о. для полноценной разработки и тестирования WebGL приложений, нужно создать локальный web-сервер и запустить его. Существует множество способов сделать это. Я решил воспользоваться функционалом, присутствующим в Python. Скачать и установить последнюю на сегодняшний день версию (Python 3.4.2) можно с официальной страницы Python. При установке я включил опцию добавления в переменную PATH пути расположения python.exe.

После установки python локальный web-сервер запускается следующим образом:

  1. Устанавливаем текущим каталог, в котором находятся интересующие нас html странички с WebGL приложениями:
    cd /D "D:\WebGL\my_sandbox\app_03"
  2. Запускаем локальный web-сервер. Например, будем слушать порт 8001:
    python -m http.server 8001
  3. В адресной строке браузера указываем следующий адрес:
    http://localhost:8001/

Если в текущем каталоге (в нашем случае - в "D:\WebGL\my_sandbox\app_03") находится файл index.html (или index.htm), то браузер автоматически отобразит его содержимое. В противном же случае он в виде списка ссылок покажет перечень файлов и подкаталогов данного каталога. Для открытия в браузере нужного html файла, обозначенного в данном списке, нужно кликнуть по его имени мышкой.

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

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

// Чтение шейдера из файла
function readShaderFile(gl, fileName, shader) {
  var request = new XMLHttpRequest();

  request.onreadystatechange = function() {
    if (request.readyState === 4 && request.status !== 404) { 
      onReadShader(gl, request.responseText, shader); 
    }
  }
  request.open('GET', fileName, true); // Создаём запрос получения файла
  request.send();                      // Отправляем запрос
}

// Шейдер загружен из файла
function onReadShader(gl, fileString, shader) {
  if (shader == 'v') { // Вершинный шейдер
    VSHADER_SOURCE = fileString;
  } else 
  if (shader == 'f') { // Фрагментный шейдер
    FSHADER_SOURCE = fileString;
  }
}

Затем эти функции мы используем в нашем коде:

// Исходный код вершинного шейдера
var VSHADER_SOURCE = null;
// Исходный код фрагментного шейдера
var FSHADER_SOURCE = null;

// Метод, который будет выполнен при срабатывании события onload элемента body в
// документе HTML5.
function main(){  
  
  // Получаем холст
  if(!(canvas = document.getElementById('webgl'))){
    console.log('Элемент с идентификатором \'webgl\' не найден.');
    return;
  }
  
  // Получаем контекст WebGL
  if (!(gl = getWebGLContext(canvas))){
    console.log('Не удалось получить контекст \'WebGL\'.');
    return;
  }
  
  // Получаем исходный код шейдеров:
  readShaderFile(gl, './shaders/point.vert', 'v');
  readShaderFile(gl, './shaders/point.frag', 'f');
  start(gl);
}

function start(gl) {
    // Выполняем инициализацию шейдеров, а так же объекта программы (gl.program)
  if(!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)){
    console.log('Не удалось инициализировать шейдеры.');
    return;
  }
  
  if(!gl.program){
    console.log('Не инициализирован объект \'gl.program\'.');
    return;
  }
  
  if((a_Position = gl.getAttribLocation(gl.program, 'a_Position')) < 0){
    console.log('Не удалось получить ссылку на \'a_Position\'.');
    return;
  }
  
  // Координаты точки, которую будем рисовать.
  gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0);
  
  // Настраиваем очистку буферов
  gl.clearColor(0.0, 0.0, 0.0, 1.0); // буфер цвета (формат RGBA)
  gl.clearDepth(1.0); // буфер глубины
  gl.clearStencil(0); // буфер трафарета
  
  // Выполняем очистку буферов согласно обозначенным выше настройкам
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
  
  // Рисуем точку
  gl.drawArrays(gl, 0, 1);
}

Однако, открыв с помощью локального web-сервера нашу html страничку в браузере мы ничего не увидим. Затем, открыв инструменты разработчика (клавиша F12 в IE или Ctrl + Shift + I в др. браузерах) мы увидим в консоли сообщение:

Failed to compile shader: ERROR: 0:1: 'null' : syntax error   cuon-utils.js:88 
Failed to compile shader: ERROR: 0:1: 'null' : syntax error   cuon-utils.js:88
Failed to create program   cuon-utils.js:12
Не удалось инициализировать шейдеры.  index.js:36

Это происходит потому, что в приведённом выше коде вызов метода request.open(...) объекта XMLHttpRequest выполняется асинхронно. Исходный код шейдеров ещё не успел обработаться в WebGL, а код JavaScript уже запустил на исполнение код функции start(gl). Поскольку к этому времени инциализация переменной FSHADER_SOURCE ещё не успела произойти, то мы и получаем ошибку инициализации шейдеров.

Исправляем ситуацию

Как вариант (не лучший), можно строку кода

request.open('GET', fileName, true);

заменить на

request.open('GET', fileName, false);

В этом случае запрос, отправляемый объектом XMLHttpRequest будет выполняться синхронно. Но из-за этого браузер может "подвисать" на время получения ответа.

Либо другой, более предпочтительный вариант: чтобы исправить ситуацию, нужно из кода функции main() убрать последнюю строку кода

start(gl);

а в конец  функции onReadShader добавить вызов start(gl) после выполнения проверки инициализации переменных, содержащих исходный код шейдеров:

// Когда оба шейдера доступны, запускаем функцию start().
if (VSHADER_SOURCE && FSHADER_SOURCE) start(gl);

Теперь браузер корректно отобразит страничку нашего WebGL-приложения.

2 комментария: