Строим 3D проекты – Шаг 8

К настоящему моменту мы уже написали программу, в которой вращается текстурированная 3D фигура. Программа выполняется на браузере клиента, но загружается с сервера. Прежде чем идти дальше, внесем некоторые улучшения: управление с клавиатуры, смешение текcтур, подключение шейдеров из файлов по технологии AJAX.
Управление с помощью клавиатуры
В программу лучше внести элементы взаимодействия с пользователем, чтобы можно было управлять определенными событиями, не исправляя и не перезапуская программу. Рассмотрим управление с клавиатуры на примере переключения отображения текстуры. Также сделаем возможность ставить вращение на паузу.
Для начала заведем переменные, хранящие состояние управляемых свойств:
var paused08pr = false; var useTexture08pr = true;
Переменная paused08pr выставлена в false, то есть, вращение изначально не на паузе.
Переменная useTexture08pr выставлена в true, то есть, текстура изначально отображается.
Мы планируем включать/отключать текстуру по нажатию клавиши ‘T’, а ставить на паузу вращение по клавише ‘Пробел’. Чтобы отследить нажатие клавиш на клавиатуре, мы добавим обработчик событий. Запишем его для удобства в отдельный файл:
<script src="js/keyboard08pr.js"></script>
Обработчик события addEventListener при нажатии клавиши вызовет нашу функцию myKeyupFunction08pr, в которой мы определим, какая клавиша нажата, и что делать:
document.addEventListener("keyup", myKeyupFunction08pr);
function myKeyupFunction08pr(evt)
{
switch(evt.keyCode)
{
case 32: //'space'
paused08pr = !paused08pr;
break;
case 84: //'t'
useTexture08pr =!useTexture08pr;
if(useTexture08pr)
{
gl08pr.uniform1i (glProgram08pr.uDoTexturing08pr, 1);
}
else
{
gl08pr.uniform1i (glProgram08pr.uDoTexturing08pr, 0);
}
break;
default:
break;
}
}
ASCII коды, например, 84 для клавиши T, можно узнать в интернете по запросу ‘ASCII таблица’.

Или использовать простую программку здесь
С включение паузы всё просто – вставляем перед увеличением угла поворота условие:
if( !paused08pr ) angle08pr += 0.005;
С отключением текстуры сложнее. По коду видно, что для отключения мы посылаем 0 в шейдер в униформу uDoTexturing08pr. Добавим эту униформу в пиксельный шейдер:
<script id="shader-fs-08pr" type="x-shader/x-fragment">
varying highp vec2 vTextureCoord08pr;
uniform sampler2D uSampler08pr;
uniform int uDoTexturing08pr;
void main(void)
{
if(uDoTexturing == 1)
{
gl_FragColor = texture2D( uSampler08pr, vec2( vTextureCoord08pr.st ) );
}
else
{
gl_FragColor = vec4(1.0, 0.1, 0.1, 1.0); //RED
}
}
</script>
В случае отключения текстуры, мы зальем фигуру красным цветом, но можно было бы и разными цветами, как на шаге 5.
В конец функции loadTexture08pr добавляем поиск униформы и задаем ей начальное значение 1 (включена текстура):
//Connect to shader.------------------------ glProgram08pr.uDoTexturing08pr = gl08pr.getUniformLocation (glProgram08pr, "uDoTexturing08pr"); gl08pr.uniform1i ( glProgram08pr.uDoTexturing08pr, 1);
Теперь становится понятно действие, которое мы задали выше для нажатия клавиши ‘T’.
С отключенной текстурой фигура (залитая одним цветом) выглядит плоской:

Наложение нескольких текстур
Если нам необходимо, чтобы на объекте были нанесены сразу текстуры из двух файлов, то придется загрузить текстуры в две отдельных переменные, а затем вывести их уже не только с помощью texture2D (), но и с помощью mix (). Для начала используем текстуру в виде надписи, чтобы было легко визуально отследить ее тексели и их направление:

Данная техтура записана в файл формата png, который позволяет не хранить фон. За счет этого, при наложении на нижнюю текстуру, мы можем выставить полную прозрачность фона, и увидеть нижнюю текстуру.
Готовим пиксельный шейдер:
<script id="shader-fs-08pr" type="x-shader/x-fragment">
varying highp vec2 vTextureCoord08pr;
uniform sampler2D uSampler08pr;
uniform sampler2D uSampler208pr;
uniform int uDoTexturing08pr;
void main(void)
{
if(uDoTexturing08pr == 1)
{
//gl_FragColor = texture2D(uSampler08pr, vec2( vTextureCoord08pr.st ) );
highp vec4 tileColor = texture2D( uSampler08pr, vec2( vTextureCoord08pr.st ));
highp vec4 logoColor = texture2D( uSampler208pr, vec2( vTextureCoord08pr.st ));
gl_FragColor = mix(tileColor, logoColor, logoColor.a);
}
else
{
gl_FragColor = vec4(1.0, 0.1, 0.1, 1.0); //RED
}
}
</script>
Мы завели вторую униформу типа sampler2D для второй текстуры. В tileColor мы берем цвет пикселя по первой текстуре, а в logoColor – соответственно по второй текстуре. В mix () мы накладываем на пиксель первой текстуры, пиксель второй текстуры. Прозрачность второй текстуры выставляем logoColor.a, которая равна нулю в местах фона, как описано выше.
В программе, как всегда, начнем с переменных. Несколько текстур и изображений удобно хранить в массивах. Также, к номеру текстуры удобно обращаться по названию, а не по числу:
var textureImage08pr = []; var texture08pr = []; var TILE_TEXTURE08pr = 0; var LOGO_TEXTURE08pr = 1;
Загружаем несколько текстур:
function loadTexture08pr()
{
//0th texture----------------------------
textureImage08pr [TILE_TEXTURE08pr] = new Image();
textureImage08pr[ TILE_TEXTURE08pr ].onload = function()
{
setupTexture08pr( TILE_TEXTURE08pr );
gl08pr.uniform1i( glProgram08pr.samplerUniform, TILE_TEXTURE08pr );
}
textureImage08pr[ TILE_TEXTURE08pr ].src = "textures/tiles-128x128.jpg";
//1st texture-----------------------------
textureImage08pr[ LOGO_TEXTURE08pr ] = new Image();
textureImage08pr[ LOGO_TEXTURE08pr ].onload = function()
{
setupTexture08pr( LOGO_TEXTURE08pr );
gl08pr.uniform1i( glProgram08pr.samplerUniform2, LOGO_TEXTURE08pr);
}
textureImage08pr[ LOGO_TEXTURE08pr].src = "textures/logo-512px.png";
//Connect to shader.------------------------
glProgram08pr.uDoTexturing08pr = gl08pr.getUniformLocation (glProgram08pr, "uDoTexturing08pr");
gl08pr.uniform1i ( glProgram08pr.uDoTexturing08pr, 1);
}
function setupTexture08pr(i)
{
gl08pr.activeTexture (gl08pr.TEXTURE0 + i);
texture08pr[i] = gl08pr.createTexture();
gl08pr.bindTexture (gl08pr.TEXTURE_2D, texture08pr[i]);
if( !gl08pr.isTexture (texture08pr[i]) )
console.error("Error: Texture is invalid");
else
{
gl08pr.pixelStorei( gl08pr.UNPACK_FLIP_Y_WEBGL, true);
gl08pr.texParameteri( gl08pr.TEXTURE_2D, gl08pr.TEXTURE_MAG_FILTER, gl08pr.NEAREST);
gl08pr.texParameteri( gl08pr.TEXTURE_2D, gl08pr.TEXTURE_MIN_FILTER, gl08pr.NEAREST);
gl08pr.texImage2D( gl08pr.TEXTURE_2D, 0, gl08pr.RGBA, gl08pr.RGBA, gl08pr.UNSIGNED_BYTE, textureImage08pr[i]);
}
}
И не забываем найти униформы типа sampler2D:
function getMatrixUniforms08pr()
{
glProgram08pr.mvMatrixUniform = gl08pr.getUniformLocation (glProgram08pr, "uMVMatrix08pr");
glProgram08pr.pMatrixUniform = gl08pr.getUniformLocation (glProgram08pr, "uPMatrix08pr");
glProgram08pr.samplerUniform = gl08pr.getUniformLocation (glProgram08pr, "uSampler08pr");
glProgram08pr.samplerUniform2 = gl08pr.getUniformLocation (glProgram08pr, "uSampler208pr");
}
Запускаем программу:

Не на всех гранях надписи нужного направления. Для исправления, нужно было бы брать вторую текстуру по отдельным координатам текстуры.
Поэкспериментируем с параметрами смешения в mix (). Напомню, начальные параметры:
gl_FragColor = mix(tileColor, logoColor, logoColor.a);
Попробуем смешение выставить 50%:
gl_FragColor = mix(tileColor, logoColor, 0.5);

Теперь попробуем поменять текстуры местами – разместить черепицу поверх надписи с данными о прозрачности из надписи:
gl_FragColor = mix(logoColor, tileColor, logoColor.a);

И наконец, используем реалистичную текстуру, например, капли дождя:

Загрузка шейдеров из отдельных файлов по AJAX
Раз уж мы все равно загружаем программу с сервера, то нам разрешено загружать оттуда и файлы с шейдерами. Хранить шейдеры в отдельных файлах удобно – их легче сравнивать друг с другом, можно использовать один шейдер для нескольких программ. Запишем шейдеры в файлы shaders/01shader08pr.vs и shaders/02shader08pr.fs. Без шейдеров, код html становиться совсем небольшим:
<!doctype html>
<html>
<head>
<title>WebGL 3D prism - texture</title>
<script src="../../js_std/gl-matrix-min.js"></script>
<script src="js/vars08pr.js"></script>
<script src="js/code08pr.js"></script>
<script src="js/keyboard08pr.js"></script>
</head>
<body >
<label id= "errorMessage76canvas08pr" style="color: red;"> </label>
<canvas id= "my-canvas08pr" class= "canvas-adaptive" width="600" height="600">
Error from Html: Your browser does not support the HTML5 canvas element.
</canvas>
</body>
</html>
Технология AJAX (asynchronous JavaScript and XML) позволяет подгрузить в нашу программу часть данных с сервера. То есть, чтобы получить какие-либо данные когда-либо, нам не нужно перезагружать страницу в браузере. Для загрузки шейдеров используем следующий шаблон:
function initShaders08pr()
{
//Get shader source.
var fs_source = null;
var vs_source = null;
//----Begin Ajax---------------------------------------------------
var xhr = new XMLHttpRequest();
//Overriding the Mime type is required.
xhr.overrideMimeType('text/xml');
//For synchronous request - false third parameter.
//VS
xhr.open('GET', 'shaders/01shader08pr.vs', false);
xhr.send(null);
if (xhr.readyState == xhr.DONE)
{
if(xhr.status === 200)
vs_source = xhr.responseXML.documentElement.firstChild.data;
else
console.error("Error: " + xhr.statusText);
}
//FS
xhr.open('GET', 'shaders/02shader08pr.fs', false);
xhr.send(null);
if (xhr.readyState == xhr.DONE)
{
if(xhr.status === 200)
fs_source = xhr.responseXML.documentElement.firstChild.data;
else
console.error("Error: " + xhr.statusText);
}
//----End Ajax---------------------------------------------------
//1.Create and compile shaders.
…
Когда шейдеры загрузятся, программа продолжит работу.
Ура, шаг-солянка из 3 разных частей пройден! Теперь, ко всему прочему, наша программа интерактивная (с управлением пользователем), фигура сразу с несколькими текстурами, шейдеры загружаются из отдельных файлов. Можно переходить к изучению источников света в следующем шаге.