1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
|
---
title: Lighting in WebGL
slug: Web/API/WebGL_API/Tutorial/Lighting_in_WebGL
translation_of: Web/API/WebGL_API/Tutorial/Lighting_in_WebGL
---
<p>{{WebGLSidebar("Tutorial")}} {{PreviousNext("Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL", "Web/API/WebGL_API/Tutorial/Animating_textures_in_WebGL")}}</p>
<p>Вам уже должно быть понятно,что у WebGL нет много "встроенного знания". Он просто выполняет две функции, которые вы написали - вершинный шейдер и фрагментный шейдер, а функции, которые рисуют сцену должны написать вы сами. Другими словами, если вы хотите добавить освещение, то вы должны рассчитать его самостоятельно. К счастью, сделать это не сложно. В этой статье мы опишем некоторые основы.</p>
<h2 id="Симуляция_освещения_и_затенения_в_3D">Симуляция освещения и затенения в 3D</h2>
<p>Хотя детали теории, лежащей в основе симуляции освещения в 3D-графике лежат далеко за пределами этой статьи, будет полезным немного узнать о том, как это работает. Посмотрите статью <a class="external" href="http://en.wikipedia.org/wiki/Phong_shading">Затенение по Фонгу</a> в Википедии, чтобы получить хороший обзор наиболее часто используемых моделей освещения. А <a href="https://webglfundamentals.org/webgl/lessons/webgl-3d-lighting-point.html">в этой статье</a> вы можете посмотреть объяснение, основанное на WebGL.</p>
<p>Существует три основных типа источников света:</p>
<p><strong>Окружающий свет</strong> освещает всю сцену. Он не направленный и освещает все грани всех объектов одинаково, не зависимо от ориентации граней.</p>
<p><strong>Направленный свет</strong> исходит из определённого направления. Этот свет приходит от настолько удалённого источника, что все фотоны летят параллельно друг другу. К примеру, солнечный свет можно считать.</p>
<p><strong>Точечный свет</strong> исходит из одной точки во всех направлениях. В реальном мире многие источники освещения являются точечными, например электрическая лампочка.</p>
<p>В этой статье мы упростим модель освещения и будем использовать только простой направленный и окружающий свет. Мы не будем создавать блики на поверхности объектов ({{interwiki("wikipedia", "specular highlights")}}) и точечные источники света. Вместо этого мы добавим окружающий свет и направленный свет в сцену с вращающимся кубом из <a href="/ru/docs/Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL">предыдущего примера</a>.</p>
<p>Если оставить в стороне блики и точечные источники света, то останутся два пункта, которые нужно изучить по порядку:</p>
<ol>
<li>Мы должны привязать <strong>нормаль поверхности</strong> к каждой вершине. Это вектор, который направлен перпендикулярно грани в данной вершине.</li>
<li>Нам нужно знать направление, в котором распространяется свет. Оно определяется <strong>вектором направления</strong>.</li>
</ol>
<p>Затем мы обновим вершинный шейдер, чтобы скорректировать цвет каждой вершины в зависимости от окружающего и направленного освещения с учётом угла падения на грань. Мы увидим, как это делается, когда посмотрим на код шейдера.</p>
<h2 id="Построение_нормали_для_вершин">Построение нормали для вершин</h2>
<p>Сначала нам нужно создать массив нормалей для всех вершин, из которых состоит наш куб. Это будет просто, потому что куб очень простой объект. Очевидно, что для более сложных объектов расчёт нормалей будет более затратным.</p>
<pre class="brush: js"> 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 {
position: positionBuffer,
normal: normalBuffer,
textureCoord: textureCoordBuffer,
indices: indexBuffer,
};
</pre>
<p>Код уже должен выглядеть узнаваемо. Мы создаём новый буфер, связываем его с рабочим буфером и записываем в него массив нормалей к вершинам при помощи <code>bufferData()</code>.</p>
<p>Затем добавим в <code>drawScene()</code> код, который свяжет массив нормалей с атрибутом шейдера. Таким образом шейдер сможет получить к нему доступ:</p>
<pre class="brush: js"> // Указываем 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);
}
</pre>
<p>В конце нужно обновить код, который строит матрицы для uniform-переменных, чтобы создать и передать в шейдер <strong>матрицу нормалей</strong>, которая используется для трансформации нормалей при расчёте ориентации куба относительно направления на источник света:</p>
<pre class="brush: js"> const normalMatrix = mat4.create();
mat4.invert(normalMatrix, modelViewMatrix);
mat4.transpose(normalMatrix, normalMatrix);
...
gl.uniformMatrix4fv(
programInfo.uniformLocations.normalMatrix,
false,
normalMatrix);
</pre>
<h2 id="Обновление_шейдеров">Обновление шейдеров</h2>
<p>Теперь у нас есть все данные для шейдеров. Пора обновить код самих шейдеров.</p>
<h3 id="Вершинный_шейдер">Вершинный шейдер</h3>
<p>Сначала обновим вершинный шейдер, чтобы он рассчитывал значение освещения для каждой вершины на основе окружающего и направленного света. Посмотрим на код:</p>
<pre class="brush: html"> 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);
}
`;
</pre>
<p>После расчёта позиции вершины мы передаём координаты текселя ({{Glossary("texel")}}), соответствующего вершине, во фрагментный шейдер, и начинаем расчёт освещения вершины.</p>
<p>Сначала нужно преобразовать нормаль, основываясь на текущей ориентации куба - умножив нормаль вершины на матрицу нормалей. Затем мы можем рассчитать количество света от направленного источника, которое приходит в вершину, посчитав скалярное произведение преобразованной нормали и вектора направления (направления, с которого приходит свет). Если скалярное произведение меньше нуля, то мы принимаем его за ноль, потому что количество света не может быть меньше 0.</p>
<p>После расчёта количества падающего направленного света мы можем посчитать финальное освещение, сложив окружающий свет и произведение количества направленного света на его цвет. В результате получается значение RGB, которое используется фрагментным шейдером для изменения цвета каждого пикселя.</p>
<h3 id="Фрагментный_шейдер">Фрагментный шейдер</h3>
<p>Фрагментный шейдер должен быть обновлён таким образом, чтобы он учитывал в значение освещения, рассчитанное в вершинном шейдере:</p>
<pre class="brush: js"> 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);
}
`;
</pre>
<p>Здесь мы получаем цвет текселя, как и в предыдущем примере, но перед тем, как установить цвет фрагмента, мы умножаем цвет текселя на значение освещения, чтобы учесть влияние источников света.</p>
<p>Осталось только посмотреть на определение атрибута <code>aVertexNormal</code> и uniform-переменной <code>uNormalMatrix</code>.</p>
<pre class="brush: js"> 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'),
},
};
</pre>
<p>И это всё!</p>
<p>{{EmbedGHLiveSample('webgl-examples/tutorial/sample7/index.html', 670, 510) }}</p>
<p><a href="https://github.com/mdn/webgl-examples/tree/gh-pages/tutorial/sample7">Посмотреть код примера полностью</a> | <a href="http://mdn.github.io/webgl-examples/tutorial/sample7/">Открыть демо в новом окне</a></p>
<h2 id="Самостоятельные_упражнения">Самостоятельные упражнения</h2>
<p>Очевидно, что это простой пример, показывающий базовое вершинное освещение. В более продвинутой графике вам наверняка захочется сделать попиксельное освещение.</p>
<p>Также вы можете поэкспериментировать с направлением на источник света, цветами окружающего и направленного света, и т. д.</p>
<p>{{PreviousNext("Web/API/WebGL_API/Tutorial/Using_textures_in_WebGL", "Web/API/WebGL_API/Tutorial/Animating_textures_in_WebGL")}}</p>
|