Не так давно в сети появился стандарт WebGL, о котором и пойдет речь ниже. Этот стандарт описывает каким образом можно использовать аппаратное ускорение графики в вашем браузере и является практически полным аналогом OpenGL ES 2.0, который есть сейчас в девяти смартфонах из десяти. Так что красивые, зрелищные игры уровня Assassin’s Creed не за горами.
Самый главный плюс для меня, как программиста, это возможность быстро написать и отладить программу без дампов памяти, исключений и прочего барахла.
В этой заметке я покажу, как сделать освещение двухмерного спрайта с использованием карты нормалей и карты бликов. Для этого нам понадобятся три текстуры:
- цветовая — определяет цвет пикселя, собственно это и есть то, что мы будет рисовать
- карта нормалей — содержит нормаль каждого пикселя, из нее будем вычислять степень освещенности текущего пикселя
- карта бликов — содержит степень отражения каждого пикселя
Эффект объема у плоского объекта достигается техникой Bump mapping. Живой пример и исходники под катом.
Для каждого пикселя вычисляется два нормализованных вектора:
- направление нормали (берется из карты нормалей)
- направление от пикселя к источнику света (источник находится на высоте равной половине ширины изображения)
Затем из скалярного произведения этих векторов берется коэффициент затемнения и умножается на цвет данного пикселя. Таким нехитрым способом обеспечивается объем у совершенно плоского полигона из двух треугольников.
"use strict"; // Обработка случаев, когда браузер не поддерживает // функцию requestAnimationFrame window.requestAnimFrame = (function() { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback, element) { window.setTimeout(callback, 1000.0/30.0); }; })(); var vertexShaderSource = [ "attribute vec2 a_position;", "attribute vec2 a_texCoord;", "", "varying vec2 v_texCoord;", "", "void main() {", " gl_Position = vec4(a_position, 0.0, 1.0);", " v_texCoord = a_texCoord;", "}" ].join("\n"); var fragmentShaderSource = [ "precision highp float;", "", "uniform sampler2D u_diffuse;", "uniform sampler2D u_normalMap;", "uniform sampler2D u_specularMap;", "uniform vec2 u_lightPos;", "", "varying vec2 v_texCoord;", "", "void main() {", " vec3 n = texture2D(u_normalMap, v_texCoord).rgb;", " n = normalize(vec3(2.0 * n.xy - 1.0, n.z));", " vec3 l = normalize(vec3(u_lightPos - v_texCoord, 0.5));", " float a = dot(n, l);", " vec3 c = texture2D(u_diffuse, v_texCoord).rgb;", " float s = texture2D(u_specularMap, v_texCoord).r;", " gl_FragColor = vec4(c * (a + s * pow(a, 32.0)), 1.0);", "}" ].join("\n"); var gl; var positionId = 0; var texCoordId = 0; var diffuseUniform = 0; var normalMapUniform = 0; var specularMapUniform = 0; var lightPosId = 0; var diffuseTex = 0; var normalMapTex = 0; var specularMapTex = 0; var lightPos = [0, 0]; var vbo = 0; var program = 0; function render() { requestAnimFrame(render); if (!diffuseTex || !diffuseTex.loaded) { return; } if (!normalMapTex || !normalMapTex.loaded) { return; } if (!specularMapTex || !specularMapTex.loaded) { return; } // Включаем шейдер gl.useProgram(program); // Используем VertexBufferObject gl.bindBuffer(gl.ARRAY_BUFFER, vbo); // Указатель на позицию вершины в vbo gl.vertexAttribPointer( positionId, 2, gl.FLOAT, false, 4 * Float32Array.BYTES_PER_ELEMENT, 0); gl.enableVertexAttribArray(positionId); // Указатель на текстурные координаты в vbo gl.vertexAttribPointer( texCoordId, 2, gl.FLOAT, false, 4 * Float32Array.BYTES_PER_ELEMENT, 2 * Float32Array.BYTES_PER_ELEMENT); gl.enableVertexAttribArray(texCoordId); // Активируем нулевой текстурный юнит gl.activeTexture(gl.TEXTURE0); // Устанавливаем в нулевой текстурный юнит текстуру diffuseTex gl.bindTexture(gl.TEXTURE_2D, diffuseTex); // Параметр u_diffuse использует нулевой текстурный юнит gl.uniform1i(diffuseUniform, 0); // Активируем первый текстурный юнит gl.activeTexture(gl.TEXTURE1); // Устанавливаем в первый текстурный юнит текстуру normalMapTex gl.bindTexture(gl.TEXTURE_2D, normalMapTex); // Параметр u_normalMap использует первый текстурный юнит gl.uniform1i(normalMapUniform, 1); gl.activeTexture(gl.TEXTURE2); gl.bindTexture(gl.TEXTURE_2D, specularMapTex); gl.uniform1i(specularMapUniform, 2); // Обновляем параметр u_lightPos gl.uniform2f(lightPosId, lightPos[0], lightPos[1]); // Рисуем TRIANGLE_STRIP из четырех вершин gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } function handleLoadedTexture(texture) { // Работает с текстурой gl.bindTexture(gl.TEXTURE_2D, texture); // Инвертируем изображение по Y gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // Загружаем текстуру в WebGL формата RGB и один байт на цвет gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, texture.image); // Линейная фильтрация gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_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.bindTexture(gl.TEXTURE_2D, null); texture.loaded = true; } function compileShader(vsSource, fsSource) { // Компилируем вершинный шейдер var vShader = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vShader, vsSource); gl.compileShader(vShader); // Компилируем фрагментный шейдер var fShader = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(fShader, fsSource); gl.compileShader(fShader); // Собираем оба шейдера в программу program = gl.createProgram(); gl.attachShader(program, vShader); gl.attachShader(program, fShader); gl.linkProgram(program); return program; } function loadTexture(url) { var texture = gl.createTexture(); texture.loaded = false; texture.image = new Image(); texture.image.onload = function() { handleLoadedTexture(texture); } texture.image.src = url; return texture; } function startRender() { var canvas = document.getElementById("webgl-canvas"); canvas.onmousemove = function(event) { var bbox = canvas.getBoundingClientRect(); var x = (event.clientX - bbox.left) * (canvas.width / bbox.width); var y = (event.clientY - bbox.top) * (canvas.height / bbox.height); lightPos[0] = x / (canvas.width); lightPos[1] = 1.0 - y / canvas.height; } gl = canvas.getContext("experimental-webgl"); gl.viewport(0, 0, canvas.width, canvas.height); // Создаем и компилируем шейдер program = compileShader(vertexShaderSource, fragmentShaderSource); // Ссылка на аттрибут a_position positionId = gl.getAttribLocation(program, "a_position"); // Ссылка на аттрибут a_texCoord texCoordId = gl.getAttribLocation(program, "a_texCoord"); // Ссылка на u_diffuse diffuseUniform = gl.getUniformLocation(program, "u_diffuse"); // Ссылка на u_normalMap normalMapUniform = gl.getUniformLocation(program, "u_normalMap"); // Ссылка на u_specularMap specularMapUniform = gl.getUniformLocation(program, "u_specularMap"); // Ссылка на u_lightPos lightPosId = gl.getUniformLocation(program, "u_lightPos"); // Создаем и загружаем текстуру с цветом diffuseTex = loadTexture("/wp-content/uploads/2014/03/color.jpg"); // Создаем и загружаем текстуру с картой нормалей normalMapTex = loadTexture("/wp-content/uploads/2014/03/normal.png"); // Создаем и загружаем карту бликов specularMapTex = loadTexture("/wp-content/uploads/2014/03/specular.jpg"); // Вершины x, y, u, v var vertices = [ -1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, ]; // Создаем VertexBufferObject vbo = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); // Поехали! render(); } |
Все текстуры для примера я нашел на сайте Käy's Blog. Если вас заинтересовала тема освещения, смотрите новую часть: Освещение куба с помощью WebGL. Модель освещения Blinn-Phong.