В этом уроке мы рассмотрим освещение вращающегося куба, сделанное с помощью WebGL.
Мы будем использовать простой направленный и окружающий свет, для этого нам нужно изучить данные пункты в следующем порядке:
- Мы должны привязать нормаль поверхности к каждой вершине. Это вектор, который направлен перпендикулярно грани в данной вершине.
- Нам нужно знать направление, в котором распространяется свет. Оно определяется вектором направления.
Также нам потребуется обновить вершинный шейдер, чтобы скорректировать цвет каждой вершины в зависимости от окружающего и направленного освещения с учётом угла падения на грань.
Построение нормали для вершинДля начала нам необходимо создать массив нормалей для всех вершин, из которых состоит наш куб.
const normalBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); const vertexNormals = [ // Передняя грань 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, // Задняя грань 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, // Верхняя грань 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // Нижняя грань 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, // Правая грань 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, // Левая грань -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0 ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW); ... return { position: positionBuffer, normal: normalBuffer, textureCoord: textureCoordBuffer, indices: indexBuffer, };
Мы создаём новый буфер, связываем его с буфером, в котором хранятся вершины нашего куба, и записываем в него массив нормалей при помощи bufferData().
Затем добавим в drawScene() код, который свяжет массив нормалей с атрибутом шейдера. Это делается для того, чтобы шейдер смог получить к нему доступ.
// Указываем WebGL как извлекать нормали из // буфера нормалей в атрибут vertexNormal. { const numComponents = 3; const type = gl.FLOAT; const normalize = false; const stride = 0; const offset = 0; gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal); gl.vertexAttribPointer( programInfo.attribLocations.vertexNormal, numComponents, type, normalize, stride, offset); gl.enableVertexAttribArray( programInfo.attribLocations.vertexNormal); }
В конце нужно обновить код, который строит матрицы для uniform-переменных, чтобы создать и передать в шейдер матрицу нормалей, которая используется для трансформации нормалей при расчёте ориентации куба относительно направления на источник света:
const normalMatrix = mat4.create(); mat4.invert(normalMatrix, modelViewMatrix); mat4.transpose(normalMatrix, normalMatrix); ... gl.uniformMatrix4fv( programInfo.uniformLocations.normalMatrix, false, normalMatrix);
Мы получили все необходимые данные, поэтому не забываем, что пора обновить код шейдеров.
Вершинный шейдерВершинный шейдер обновляем таким образом, чтобы он, на основе направленного и окружающего света, рассчитывал освещение на каждой вершине куба.
const vsSource = ` attribute vec4 aVertexPosition; attribute vec3 aVertexNormal; attribute vec2 aTextureCoord; uniform mat4 uNormalMatrix; uniform mat4 uModelViewMatrix; uniform mat4 uProjectionMatrix; varying highp vec2 vTextureCoord; varying highp vec3 vLighting; void main(void) { gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; vTextureCoord = aTextureCoord; // Применяем эффект освещения highp vec3 ambientLight = vec3(0.3, 0.3, 0.3); highp vec3 directionalLightColor = vec3(1, 1, 1); highp vec3 directionalVector = normalize(vec3(0.85, 0.8, 0.75)); highp vec4 transformedNormal = uNormalMatrix * vec4(aVertexNormal, 1.0); highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0); vLighting = ambientLight + (directionalLightColor * directional); } `;
Мы рассчитываем позицию вершины, передаём её во фрагментный шейдер, после этого можем произвести расчёт освещения вершины. Для начала преобразовываем нормаль: умножаем нормаль вершины на матрицу нормалей. Вычисляем количество света от направленного источника и в конце мы можем посчитать полное освещение, сложив окружающий свет и произведение количества направленного света на его цвет. Фрагментный шейдер
В фрагментном шейдере всё проще: необходимо, чтобы он учитывал значение освещения, полученное в вершинном шейдере.
const fsSource = ` varying highp vec2 vTextureCoord; varying highp vec3 vLighting; uniform sampler2D uSampler; void main(void) { highp vec4 texelColor = texture2D(uSampler, vTextureCoord); gl_FragColor = vec4(texelColor.rgb * vLighting, texelColor.a); } `;
Мы получаем цвет текселя и умножаем на значение освещения, чтобы учесть влияние источников света.
Осталось только посмотреть на определение атрибута aVertexNormal и uniform-переменной uNormalMatrix.
const programInfo = { program: shaderProgram, attribLocations: { vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'), vertexNormal: gl.getAttribLocation(shaderProgram, 'aVertexNormal'), textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'), }, uniformLocations: { projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'), modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'), normalMatrix: gl.getUniformLocation(shaderProgram, 'uNormalMatrix'), uSampler: gl.getUniformLocation(shaderProgram, 'uSampler'), }, };
Ниже вы можете увидеть результат выполнения программы:
Прикрепленный файл | Размер |
---|---|
lighting_in_webgl_Nosalik.rar | 5.86 кб |