/
engineWebGL.js
429 lines (379 loc) 路 15.7 KB
/
engineWebGL.js
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
/**
* LittleJS WebGL Interface
* - All webgl used by the engine is wrapped up here
* - For normal stuff you won't need to see or call anything in this file
* - For advanced stuff there are helper functions to create shaders, textures, etc
* - Can be disabled with glEnable to revert to 2D canvas rendering
* - Batches sprite rendering on GPU for incredibly fast performance
* - Sprite transform math is done in the shader where possible
* - Supports shadertoy style post processing shaders
* @namespace WebGL
*/
'use strict';
/** The WebGL canvas which appears above the main canvas and below the overlay canvas
* @type {HTMLCanvasElement}
* @memberof WebGL */
let glCanvas;
/** 2d context for glCanvas
* @type {WebGLRenderingContext}
* @memberof WebGL */
let glContext;
// WebGL internal variables not exposed to documentation
let glActiveTexture, glShader, glArrayBuffer, glPositionData, glColorData, glBatchCount, glBatchAdditive, glAdditive;
///////////////////////////////////////////////////////////////////////////////
// Initalize WebGL, called automatically by the engine
function glInit()
{
// create the canvas and textures
glCanvas = document.createElement('canvas');
glContext = glCanvas.getContext('webgl2');
// some browsers are much faster without copying the gl buffer so we just overlay it instead
glOverlay && document.body.appendChild(glCanvas);
// setup vertex and fragment shaders
glShader = glCreateProgram(
'#version 300 es\n' + // specify GLSL ES version
'precision highp float;'+ // use highp for better accuracy
'uniform mat4 m;'+ // transform matrix
'in vec2 p,t;'+ // position, uv
'in vec4 c,a;'+ // color, additiveColor
'out vec4 v,d,e;'+ // return uv, color, additiveColor
'void main(){'+ // shader entry point
'gl_Position=m*vec4(p,1,1);'+ // transform position
'v=vec4(t,p);d=c;e=a;'+ // pass stuff to fragment shader
'}' // end of shader
,
'#version 300 es\n' + // specify GLSL ES version
'precision highp float;'+ // use highp for better accuracy
'in vec4 v,d,e;'+ // uv, color, additiveColor
'uniform sampler2D s;'+ // texture
'out vec4 c;'+ // out color
'void main(){'+ // shader entry point
'c=texture(s,v.xy)*d+e;'+ // modulate texture by color plus additive
'}' // end of shader
);
// init buffers
const vertexData = new ArrayBuffer(gl_VERTEX_BUFFER_SIZE);
glArrayBuffer = glContext.createBuffer();
glPositionData = new Float32Array(vertexData);
glColorData = new Uint32Array(vertexData);
glBatchCount = 0;
}
// Setup render each frame, called automatically by engine
function glPreRender()
{
// clear and set to same size as main canvas
glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
glContext.clear(gl_COLOR_BUFFER_BIT);
// set up the shader
glContext.useProgram(glShader);
glContext.activeTexture(gl_TEXTURE0);
glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = textureInfos[0].glTexture);
glContext.bindBuffer(gl_ARRAY_BUFFER, glArrayBuffer);
glContext.bufferData(gl_ARRAY_BUFFER, gl_VERTEX_BUFFER_SIZE, gl_DYNAMIC_DRAW);
glSetBlendMode();
// set vertex attributes
let offset = 0;
const initVertexAttribArray = (name, type, typeSize, size, normalize=0)=>
{
const location = glContext.getAttribLocation(glShader, name);
glContext.enableVertexAttribArray(location);
glContext.vertexAttribPointer(location, size, type, normalize, gl_VERTEX_BYTE_STRIDE, offset);
offset += size*typeSize;
}
initVertexAttribArray('p', gl_FLOAT, 4, 2); // position
initVertexAttribArray('t', gl_FLOAT, 4, 2); // texture coords
initVertexAttribArray('c', gl_UNSIGNED_BYTE, 1, 4, 1); // color
initVertexAttribArray('a', gl_UNSIGNED_BYTE, 1, 4, 1); // additiveColor
// build the transform matrix
const sx = 2 * cameraScale / mainCanvas.width;
const sy = 2 * cameraScale / mainCanvas.height;
glContext.uniformMatrix4fv(glContext.getUniformLocation(glShader, 'm'), 0,
new Float32Array([
sx, 0, 0, 0,
0, sy, 0, 0,
1, 1, -1, 1,
-1-sx*cameraPos.x, -1-sy*cameraPos.y, 0, 0
])
);
}
/** Set the WebGl blend mode, normally you should call setBlendMode instead
* @param {Boolean} [additive=0]
* @memberof WebGL */
function glSetBlendMode(additive=0)
{
// setup blending
glAdditive = additive;
}
/** Set the WebGl texture, called automatically if using multiple textures
* - This may also flush the gl buffer resulting in more draw calls and worse performance
* @param {WebGLTexture} texture
* @memberof WebGL */
function glSetTexture(texture)
{
// must flush cache with the old texture to set a new one
if (texture == glActiveTexture)
return;
glFlush();
glContext.bindTexture(gl_TEXTURE_2D, glActiveTexture = texture);
}
/** Compile WebGL shader of the given type, will throw errors if in debug mode
* @param {String} source
* @param type
* @return {WebGLShader}
* @memberof WebGL */
function glCompileShader(source, type)
{
// build the shader
const shader = glContext.createShader(type);
glContext.shaderSource(shader, source);
glContext.compileShader(shader);
// check for errors
if (debug && !glContext.getShaderParameter(shader, gl_COMPILE_STATUS))
throw glContext.getShaderInfoLog(shader);
return shader;
}
/** Create WebGL program with given shaders
* @param {WebGLShader} vsSource
* @param {WebGLShader} fsSource
* @return {WebGLProgram}
* @memberof WebGL */
function glCreateProgram(vsSource, fsSource)
{
// build the program
const program = glContext.createProgram();
glContext.attachShader(program, glCompileShader(vsSource, gl_VERTEX_SHADER));
glContext.attachShader(program, glCompileShader(fsSource, gl_FRAGMENT_SHADER));
glContext.linkProgram(program);
// check for errors
if (debug && !glContext.getProgramParameter(program, gl_LINK_STATUS))
throw glContext.getProgramInfoLog(program);
return program;
}
/** Create WebGL texture from an image and set the texture settings
* @param {Image} image
* @return {WebGLTexture}
* @memberof WebGL */
function glCreateTexture(image)
{
// build the texture
const texture = glContext.createTexture();
glContext.bindTexture(gl_TEXTURE_2D, texture);
image && image.width && glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, image);
// use point filtering for pixelated rendering
const filter = canvasPixelated ? gl_NEAREST : gl_LINEAR;
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MIN_FILTER, filter);
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_MAG_FILTER, filter);
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_S, gl_CLAMP_TO_EDGE);
glContext.texParameteri(gl_TEXTURE_2D, gl_TEXTURE_WRAP_T, gl_CLAMP_TO_EDGE);
return texture;
}
/** Draw all sprites and clear out the buffer, called automatically by the system whenever necessary
* @memberof WebGL */
function glFlush()
{
if (!glBatchCount) return;
const destBlend = glBatchAdditive ? gl_ONE : gl_ONE_MINUS_SRC_ALPHA;
glContext.blendFuncSeparate(gl_SRC_ALPHA, destBlend, gl_ONE, destBlend);
glContext.enable(gl_BLEND);
// draw all the sprites in the batch and reset the buffer
glContext.bufferSubData(gl_ARRAY_BUFFER, 0,
glPositionData.subarray(0, glBatchCount * gl_INDICIES_PER_VERT));
glContext.drawArrays(gl_TRIANGLE_STRIP, 0, glBatchCount);
glBatchCount = 0;
glBatchAdditive = glAdditive;
}
/** Draw any sprites still in the buffer, copy to main canvas and clear
* @param {CanvasRenderingContext2D} context
* @param {Boolean} [forceDraw=0]
* @memberof WebGL */
function glCopyToContext(context, forceDraw)
{
if (!glBatchCount && !forceDraw) return;
glFlush();
// do not draw in overlay mode because the canvas is visible
if (!glOverlay || forceDraw)
context.drawImage(glCanvas, 0, 0);
}
/** Add a sprite to the gl draw list, used by all gl draw functions
* @param {Number} x
* @param {Number} y
* @param {Number} sizeX
* @param {Number} sizeY
* @param {Number} angle
* @param {Number} uv0X
* @param {Number} uv0Y
* @param {Number} uv1X
* @param {Number} uv1Y
* @param {Number} rgba
* @param {Number} [rgbaAdditive=0]
* @memberof WebGL */
function glDraw(x, y, sizeX, sizeY, angle, uv0X, uv0Y, uv1X, uv1Y, rgba, rgbaAdditive=0)
{
// flush if there is not enough room or if different blend mode
const vertCount = 6;
if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
glFlush();
// prepare to create the verts from size and angle
const c = Math.cos(angle)/2, s = Math.sin(angle)/2;
const cx = c*sizeX, cy = c*sizeY, sx = s*sizeX, sy = s*sizeY;
const positionData =
[
x-cx+sy, y+cy+sx, uv0X, uv0Y,
x-cx-sy, y-cy+sx, uv0X, uv1Y,
x+cx+sy, y+cy-sx, uv1X, uv0Y,
x+cx-sy, y-cy-sx, uv1X, uv1Y,
];
// setup 2 triangle strip quad
for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
{
let j = clamp(i-1, 0, 3)*4; // degenerate tri at ends
glPositionData[offset++] = positionData[j++];
glPositionData[offset++] = positionData[j++];
glPositionData[offset++] = positionData[j++];
glPositionData[offset++] = positionData[j++];
glColorData[offset++] = rgba;
glColorData[offset++] = rgbaAdditive;
}
glBatchCount += vertCount;
}
/** Add a convex polygon to the gl draw list
* @param {Array} points - Array of Vector2 points
* @param {Number} rgba - Color of the polygon
* @memberof WebGL */
function glDrawPoints(points, rgba)
{
// flush if there is not enough room or if different blend mode
const vertCount = points.length + 2;
if (glBatchCount >= gl_MAX_BATCH-vertCount || glBatchAdditive != glAdditive)
glFlush();
// setup triangle strip from list of points
for(let i = vertCount, offset = glBatchCount * gl_INDICIES_PER_VERT; i--;)
{
const j = clamp(i-1, 0, vertCount-3); // degenerate tri at ends
const h = j>>1;
const point = points[j%2? h : vertCount-3-h];
glPositionData[offset++] = point.x;
glPositionData[offset++] = point.y;
glPositionData[offset++] = 0; // uvx
glPositionData[offset++] = 0; // uvy
glColorData[offset++] = 0; // nothing to tint
glColorData[offset++] = rgba; // apply rgba via additive
}
glBatchCount += vertCount;
}
///////////////////////////////////////////////////////////////////////////////
// post processing - can be enabled to pass other canvases through a final shader
let glPostShader, glPostArrayBuffer, glPostTexture, glPostIncludeOverlay;
/** Set up a post processing shader
* @param {String} shaderCode
* @param {Boolean} includeOverlay
* @memberof WebGL */
function glInitPostProcess(shaderCode, includeOverlay)
{
ASSERT(!glPostShader); // can only have 1 post effects shader
if (!shaderCode) // default shader pass through
shaderCode = 'void mainImage(out vec4 c,vec2 p){c=texture(iChannel0,p/iResolution.xy);}';
// create the shader
glPostShader = glCreateProgram(
'#version 300 es\n' + // specify GLSL ES version
'precision highp float;'+ // use highp for better accuracy
'in vec2 p;'+ // position
'void main(){'+ // shader entry point
'gl_Position=vec4(p,1,1);'+ // set position
'}' // end of shader
,
'#version 300 es\n' + // specify GLSL ES version
'precision highp float;'+ // use highp for better accuracy
'uniform sampler2D iChannel0;'+ // input texture
'uniform vec3 iResolution;'+ // size of output texture
'uniform float iTime;'+ // time passed
'out vec4 c;'+ // out color
'\n' + shaderCode + '\n'+ // insert custom shader code
'void main(){'+ // shader entry point
'mainImage(c,gl_FragCoord.xy);'+ // call post process function
'c.a=1.;'+ // always use full alpha
'}' // end of shader
);
// create buffer and texture
glPostArrayBuffer = glContext.createBuffer();
glPostTexture = glCreateTexture();
glPostIncludeOverlay = includeOverlay;
// hide the original 2d canvas
mainCanvas.style.visibility = 'hidden';
}
// Render the post processing shader, called automatically by the engine
function glRenderPostProcess()
{
if (!glPostShader)
return;
// prepare to render post process shader
if (glEnable)
{
glFlush(); // clear out the buffer
mainContext.drawImage(glCanvas, 0, 0); // copy to the main canvas
}
else // set viewport
glContext.viewport(0, 0, glCanvas.width = mainCanvas.width, glCanvas.height = mainCanvas.height);
if (glPostIncludeOverlay)
{
// copy overlay canvas so it will be included in post processing
mainContext.drawImage(overlayCanvas, 0, 0);
// clear overlay canvas
overlayCanvas.width = mainCanvas.width;
}
// setup shader program to draw one triangle
glContext.useProgram(glPostShader);
glContext.disable(gl_BLEND);
glContext.bindBuffer(gl_ARRAY_BUFFER, glPostArrayBuffer);
glContext.bufferData(gl_ARRAY_BUFFER, new Float32Array([-3,1,1,-3,1,1]), gl_STATIC_DRAW);
glContext.pixelStorei(gl_UNPACK_FLIP_Y_WEBGL, true);
// set textures, pass in the 2d canvas and gl canvas in separate texture channels
glContext.activeTexture(gl_TEXTURE0);
glContext.bindTexture(gl_TEXTURE_2D, glPostTexture);
glContext.texImage2D(gl_TEXTURE_2D, 0, gl_RGBA, gl_RGBA, gl_UNSIGNED_BYTE, mainCanvas);
// set vertex position attribute
const vertexByteStride = 8;
const pLocation = glContext.getAttribLocation(glPostShader, 'p');
glContext.enableVertexAttribArray(pLocation);
glContext.vertexAttribPointer(pLocation, 2, gl_FLOAT, 0, vertexByteStride, 0);
// set uniforms and draw
const uniformLocation = (name)=>glContext.getUniformLocation(glPostShader, name);
glContext.uniform1i(uniformLocation('iChannel0'), 0);
glContext.uniform1f(uniformLocation('iTime'), time);
glContext.uniform3f(uniformLocation('iResolution'), mainCanvas.width, mainCanvas.height, 1);
glContext.drawArrays(gl_TRIANGLE_STRIP, 0, 3);
}
///////////////////////////////////////////////////////////////////////////////
// store gl constants as integers so their name doesn't use space in minifed
const
gl_ONE = 1,
gl_TRIANGLE_STRIP = 5,
gl_SRC_ALPHA = 770,
gl_ONE_MINUS_SRC_ALPHA = 771,
gl_BLEND = 3042,
gl_TEXTURE_2D = 3553,
gl_UNSIGNED_BYTE = 5121,
gl_FLOAT = 5126,
gl_RGBA = 6408,
gl_NEAREST = 9728,
gl_LINEAR = 9729,
gl_TEXTURE_MAG_FILTER = 10240,
gl_TEXTURE_MIN_FILTER = 10241,
gl_TEXTURE_WRAP_S = 10242,
gl_TEXTURE_WRAP_T = 10243,
gl_COLOR_BUFFER_BIT = 16384,
gl_CLAMP_TO_EDGE = 33071,
gl_TEXTURE0 = 33984,
gl_ARRAY_BUFFER = 34962,
gl_STATIC_DRAW = 35044,
gl_DYNAMIC_DRAW = 35048,
gl_FRAGMENT_SHADER = 35632,
gl_VERTEX_SHADER = 35633,
gl_COMPILE_STATUS = 35713,
gl_LINK_STATUS = 35714,
gl_UNPACK_FLIP_Y_WEBGL = 37440,
// constants for batch rendering
gl_INDICIES_PER_VERT = 6,
gl_MAX_BATCH = 1e5,
gl_VERTEX_BYTE_STRIDE = (4 * 2) * 2 + (4) * 2, // vec2 * 2 + (char * 4) * 2
gl_VERTEX_BUFFER_SIZE = gl_MAX_BATCH * gl_VERTEX_BYTE_STRIDE;