Освещение в WebGL
Как уже должно быть понятно, у WebGL нет большого встроенного знания. Он просто выполняет две предоставленные вами функции — вершинный и фрагментный шейдеры — и ожидает, что вы напишете творческие функции, чтобы получить желаемые результаты. Другими словами, если вам нужно освещение, вы должны рассчитать его самостоятельно. К счастью, это не так сложно, и эта статья охватывает некоторые основы.
Моделирование освещения и затенения в 3D
Хотя подробное изучение теории моделирования освещения в 3D-графике выходит за рамки этой статьи, полезно знать, как это работает. Вместо глубокого обсуждения здесь, ознакомьтесь со статьей о затенении Фонга на Википедии, которая предоставляет хороший обзор наиболее часто используемой модели освещения, или, если вы хотите увидеть объяснение на основе WebGL, посмотрите эту статью.
Существует три основных типа освещения:
Фоновое освещение — это освещение, пронизывающее сцену; оно не направлено и одинаково влияет на все грани в сцене, независимо от направления.
Направленное освещение — это освещение, исходящее из определенного направления. Это освещение, исходящее от настолько далекого источника, что каждый фотон движется параллельно всем другим фотонам. Например, солнечный свет считается направленным освещением.
Точечное освещение — это освещение, исходящее из точки, излучаемое во всех направлениях. Именно так обычно работают многие реальные источники света. Например, лампочка излучает свет во всех направлениях.
В наших целях мы упростим модель освещения, рассматривая только простое направленное и фоновое освещение; у нас не будет никаких зеркальных бликов или точечных источников света в этой сцене. Вместо этого у нас будет фоновое освещение и один источник направленного света, направленный на вращающийся куб из предыдущей демонстрации.
После исключения понятия точечных источников и зеркального освещения нам понадобятся две вещи для реализации направленного освещения:
- Нам нужно связать с каждой вершиной нормаль поверхности. Это вектор, перпендикулярный грани в этой вершине.
- Нам нужно знать направление распространения света; это определено вектором направления.
Затем мы обновим вершинный шейдер, чтобы настроить цвет каждой вершины, учитывая фоновое освещение, а также эффект направленного освещения, учитывая угол его падения на грань. Мы увидим, как это сделать, когда посмотрим на код шейдера.
Создание нормалей для вершин
Первое, что нам нужно сделать, — это сгенерировать массив нормалей для всех вершин, составляющих наш куб. Поскольку куб — очень простой объект, это легко сделать; очевидно, для более сложных объектов вычисление нормалей будет более сложным.
Примечание: Добавьте эту функцию в свой модуль "init-buffer.js":
function initNormalBuffer(gl) {
const normalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
const vertexNormals = [
// Front
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
// Back
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,
// Top
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
// Bottom
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,
// Right
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
// Left
-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 normalBuffer;
}
Это должно показаться знакомым; мы создаем новый буфер, связываем его как используемый буфер, затем отправляем в буфер наш массив нормалей вершин, вызвав bufferData().
Как и прежде, мы обновили initBuffers() для вызова нашей новой функции и для возвращения созданного ею буфера.
Примечание: В конце вашей функции initBuffers(), добавьте следующий код, заменив существующее выражение return:
const normalBuffer = initNormalBuffer(gl);
return {
position: positionBuffer,
normal: normalBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
Затем мы добавим код в модуль "draw-scene.js", чтобы связать массив нормалей с атрибутом шейдера, чтобы код шейдера мог получить к нему доступ.
Примечание: Добавьте эту функцию в свой модуль "draw-scene.js":
// Tell WebGL how to pull out the normals from
// the normal buffer into the vertexNormal attribute.
function setNormalAttribute(gl, buffers, programInfo) {
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);
}
Примечание: Добавьте эту строку в функцию drawScene() вашего модуля "draw-scene.js", непосредственно перед строкой gl.useProgram():
setNormalAttribute(gl, buffers, programInfo);
Наконец, нам нужно обновить код, создающий униформные матрицы, чтобы сгенерировать и передать шейдеру матрицу нормалей, которая используется для преобразования нормалей при работе с текущей ориентацией куба по отношению к источнику света.
Примечание: Добавьте следующий код к функции drawScene() вашего модуля "draw-scene.js", сразу после трех вызовов mat4.rotate():
const normalMatrix = mat4.create(); mat4.invert(normalMatrix, modelViewMatrix); mat4.transpose(normalMatrix, normalMatrix);
Примечание: Добавьте следующий код к функции drawScene() вашего модуля "draw-scene.js", сразу после двух предыдущих вызовов gl.uniformMatrix4fv():
gl.uniformMatrix4fv( programInfo.uniformLocations.normalMatrix, false, normalMatrix, );
Обновление шейдеров
Теперь, когда все данные, необходимые шейдерам, доступны, нам нужно обновить код в самих шейдерах.
Вершинный шейдер
Первое, что нужно сделать, — обновить вершинный шейдер, чтобы он генерировал значение затенения для каждой вершины, основываясь на фоновом освещении и направленном освещении.
Примечание: Обновите объявление vsSource в своей функции main() следующим образом:
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;
// Apply lighting effect
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);
}
`;
После вычисления положения вершины и передачи координат пикселя, соответствующих вершине, в фрагментный шейдер, мы можем заняться вычислением затенения для вершины.
Первое, что мы делаем, — преобразуем нормаль на основе текущей ориентации куба, умножив нормаль вершины на матрицу нормалей. Затем мы можем вычислить количество направленного освещения, которое нужно применить к вершине, вычислив скалярное произведение преобразованной нормали и вектора направления (то есть направление, из которого исходит свет). Если это значение меньше нуля, мы фиксируем его на нуле, так как освещенность не может быть отрицательной.
После вычисления количества направленного освещения мы можем получить значение освещенности, взяв фоновое освещение и добавив к нему произведение цвета направленного света и количества направленного освещения. В результате мы получим значение RGB, которое будет использовано фрагментным шейдером для корректировки цвета каждого рендерируемого пикселя.
Фрагментный шейдер
Фрагментный шейдер теперь должен быть обновлен для учета значения освещенности, вычисленного вершинным шейдером.
Примечание: Обновите объявление fsSource в вашей функции main() следующим образом:
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 и униформа uNormalMatrix.
Примечание: Обновите объявление programInfo в вашей функции main() следующим образом:
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"),
},
};
И на этом все!
Посмотреть весь код | Открыть эту демонстрацию в новой вкладке
Упражнения для читателя
Очевидно, это простой пример, реализующий базовое освещение по вершинам. Для более продвинутой графики вам потребуется реализовать освещение по пикселям, но это направит вас в нужном направлении.
Вы также можете попробовать поэкспериментировать с направлением источника света, цветами источников света и так далее.
© 2005–2024 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Lighting_in_WebGL