WebGL Simple 2D Example 15

Speed Up with shorts and bytes.

My texture:

Full JavaScript code on this page: (or you can View Page Source)

var gl, glExtension, canvas, vertices, verticesAsShort, verticesAsByte, vertexIndex=0
// UPDATE: Each image takes 28 bytes (4 floats of image pos + 4 shorts of texPos + 4 bytes rgba) 16+8+4 var bytesPerImage=28
// UPDATE: Let's draw lots of images! var guys = [] for(var i=0; i<10000; i++) { var size = Math.random()*99+9 guys.push({ x:Math.random()*600, y:Math.random()*600, width:size, height:size, r:Math.random()*255, g:Math.random()*255, b:Math.random()*255, a:1-Math.random()*Math.random(), speed:Math.random()*5 }) }
function drawImage(x, y, width, height, texX, texY, texWidth, texHeight, r, g, b, a) { // Set 12 slots in vertices. Overwrite what was there last frame. var i = vertexIndex vertices[i++] = x vertices[i++] = y vertices[i++] = width vertices[i++] = height
// UPDATE: multiply by 2 to get to the same position in verticesAsShort because it has twice as many slots. i *= 2 verticesAsShort[i++] = texX verticesAsShort[i++] = texY verticesAsShort[i++] = texWidth verticesAsShort[i++] = texHeight
// UPDATE: multiply by 2 to get to the same position in verticesAsByte because it has twice as many slots. i *= 2 verticesAsByte[i++] = r verticesAsByte[i++] = g verticesAsByte[i++] = b verticesAsByte[i++] = a*255
vertexIndex += bytesPerImage } // Draw rectangle by drawing the white pixel in our PNG. function drawRectangle(x, y, width, height, r, g, b, a) { drawImage(x, y, width, height, 1, 1, 1, 1, r, g, b, a) } function gameLoop() { window.requestAnimationFrame(gameLoop) // Draw a wide rectangle. drawRectangle(0,370, 200,50, 200,100,10,1)
// UPDATE: Draw the moving images. for(var i=0; i < guys.length; i++) { var guy = guys[i] var frame = (guy.x*.07+i)&1 drawImage( guy.x,guy.y, guy.width,guy.height, frame*32,32, 32,32, guy.r, guy.g, guy.b, guy.a ) guy.x+=guy.speed if(guy.x > 500)guy.x-=500 guy.y++ if(guy.y > 500)guy.y-=500 }
glRender() } function glRender() { // Clear the screen. gl.clear(gl.COLOR_BUFFER_BIT) // Send to webGL the section of vertices[] that we acually used this frame. gl.bufferSubData(gl.ARRAY_BUFFER, 0, verticesAsByte.subarray(0,vertexIndex)) // Draw all the images. // 6 is the amount of points per image since they are made of 2 triangles. // vertexIndex/bytesPerImage is the amount of images we drew this frame. glExtension.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, vertexIndex/bytesPerImage) // Reset vertexIndex. We overwrite our vertices[] array every frame. vertexIndex = 0 } function glSetup() { gl = myCanvas.getContext("experimental-webgl") // This extension allows us to repeat the draw vertex operation 6 times (to make 2 triangles) on the same 12 slots in // vertices[] so we only have to put the image data into vertices[] once for each image each time we want to draw an image. glExtension = gl.getExtension("ANGLE_instanced_arrays") // Set the background color to sky blue. gl.clearColor(.5, .7, 1, 1) // Vertex shader source code. var vertCode = "attribute vec2 coordinates;" + "attribute vec2 drawSize;" + "attribute vec2 whichCorner;" + "attribute vec4 rgba;" + "attribute vec2 texPos;" + "attribute vec2 texPartSize;" + "varying highp vec4 rgbaForFrag;" + "varying highp vec2 texPosForFrag;" + "uniform vec2 canvasSize;" + "uniform vec2 texSize;" + "void main(void) {" + " vec2 drawPos;" + // Calculate which of the 4 corners of the image we are drawing now. // Then divide the position by our current canvas size. " drawPos = (coordinates + drawSize*whichCorner) / canvasSize * 2.0;" + // We are passing in only 2D coordinates. Then Z is always 0.0 and the divisor is always 1.0 " gl_Position = vec4(drawPos.x - 1.0, 1.0 - drawPos.y, 0.0, 1.0);" + // Pass the color and transparency to the fragment shader. " rgbaForFrag = rgba / 255.0;" + // Pass the texture position to the fragment shader. Now supports 4 corners, all with the same vertex data. " texPosForFrag = (texPos + texPartSize*whichCorner) / texSize;" + "}" // Create a vertex shader object. var vertShader = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vertShader, vertCode) gl.compileShader(vertShader) console.log(gl.getShaderInfoLog(vertShader)) // Fragment shader source code. var fragCode = "varying highp vec4 rgbaForFrag;" + "varying highp vec2 texPosForFrag;" + "uniform sampler2D sampler;" + "void main(void) {" + " gl_FragColor = texture2D(sampler, texPosForFrag) * rgbaForFrag;" + "}" // Create fragment shader object. var fragShader = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(fragShader, fragCode) gl.compileShader(fragShader) console.log(gl.getShaderInfoLog(fragShader)) // Tell webGL to use both my shaders. shaderProgram = gl.createProgram() gl.attachShader(shaderProgram, vertShader) gl.attachShader(shaderProgram, fragShader) gl.linkProgram(shaderProgram) gl.useProgram(shaderProgram) // Tell webGL when drawing a triangle to access the same vertex data twice so we can pass in 4 corners of a rectangle, // to draw 2 triangles which would normally need 6 points worth of vertex data. // Map triangle vertexes to our multiplier array, for which corner of the image drawn's rectangle each triangle point is at. // This shows what order to access the gl.ARRAY_BUFFER below, so it can still make 2 triangles using those 4 points, // so first it will make a point at the top-left, then the bottom-left, then the top-right, then (the second triangle starts) // the top-right again, then bottom-left again, then bottom right (which is index 3 which refers to 1,1 in my ARRAY_BUFFER). gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0, 1, 2, 2, 1, 3]), gl.STATIC_DRAW) // Our multiplier array for width/height so we can get to each corner of the image drawn. // index 0 has 0,0 which will be the top left of the image we are drawing because size will get multiplied to 0. // index 1 has 0,1 which will be the bottom left of the image we are drawing since width will get *0 but height gets *1. etc. gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()) gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0,0, 0,1, 1,0, 1,1]), gl.STATIC_DRAW) // whichCorner will access the array above, so we can get to each point in the rectangle image we want to draw. // draw size will be multiplied by this so 0,0 will be the top left of the image, and 1,1 will be the bottom right. var attribute = gl.getAttribLocation(shaderProgram, "whichCorner") gl.enableVertexAttribArray(attribute) gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 0, 0) // Now set a default array buffer to read our vertices[] from. gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()) // Make a buffer big enough to have all the data for the max images we can show at the same time. var arrayBuffer = new ArrayBuffer(100000 * bytesPerImage) gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.DYNAMIC_DRAW) vertices = new Float32Array(arrayBuffer)
// UPDATE: Make it so we can access the array as short and byte also. verticesAsShort = new Int16Array(arrayBuffer) verticesAsByte = new Uint8Array(arrayBuffer)
// Tell webGL to read 2 floats from the vertex array for each vertex // and store them in my vec2 shader variable I've named "coordinates" var attribute = gl.getAttribLocation(shaderProgram, "coordinates") gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, bytesPerImage, 0) gl.enableVertexAttribArray(attribute) glExtension.vertexAttribDivisorANGLE(attribute, 1) // Then read width and height from the vertex array and store them in "drawSize". var attribute = gl.getAttribLocation(shaderProgram, "drawSize") gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, bytesPerImage, 8) gl.enableVertexAttribArray(attribute) glExtension.vertexAttribDivisorANGLE(attribute, 1) // Then read texX,texY as short from the vertex array and store them in "texPos". var attribute = gl.getAttribLocation(shaderProgram, "texPos")
// UPDATE: changed to short. gl.vertexAttribPointer(attribute, 2, gl.SHORT, false, bytesPerImage, 16)
gl.enableVertexAttribArray(attribute) glExtension.vertexAttribDivisorANGLE(attribute, 1) // Also read 2 slots for size of the part of the texture you are drawing. var attribute = gl.getAttribLocation(shaderProgram, "texPartSize")
// UPDATE: changed to short. gl.vertexAttribPointer(attribute, 2, gl.SHORT, false, bytesPerImage, 20)
gl.enableVertexAttribArray(attribute) glExtension.vertexAttribDivisorANGLE(attribute, 1) // Then read r,g,b,a as byte from my vertices[] array and store them in "rgba". var attribute = gl.getAttribLocation(shaderProgram, "rgba")
// UPDATE: changed to byte. gl.vertexAttribPointer(attribute, 4, gl.UNSIGNED_BYTE, false, bytesPerImage, 24)
gl.enableVertexAttribArray(attribute) glExtension.vertexAttribDivisorANGLE(attribute, 1) // Tell webGL that when we set the opacity, it should be semi transparent above what was already drawn. gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) gl.enable(gl.BLEND) var image = new Image() image.onload = function() { // Create a gl texture from our JS image object. gl.bindTexture(gl.TEXTURE_2D, gl.createTexture()) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) gl.activeTexture(gl.TEXTURE0) // Tell gl that when draw images scaled up, smooth it. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) // Save texture dimensions in our shader. gl.uniform2f(gl.getUniformLocation(shaderProgram, "texSize"), image.width, image.height) } image.src = "tiles.png" // Call onresize to set the initial canvas size. window.onresize() } window.onresize = function() { // To test we'll make the canvas be 1/3 the browser width. Try resizing your browser. var width = Math.floor(innerWidth / 3) var height = 500 myCanvas.style.width = width+"px" myCanvas.style.height = height+"px" myCanvas.setAttribute("width", width) myCanvas.setAttribute("height", height) // Set the viewport size to be the whole canvas. gl.viewport(0, 0, width, height) // Set our shader variable for canvas size. It's a vec2 that holds both width and height. gl.uniform2f(gl.getUniformLocation(shaderProgram, "canvasSize"), width, height) } window.onload = function() { glSetup() gameLoop() }
HTML: