Full JavaScript code on this page: (or you can View Page Source)
var gl, glExtension, canvas, vertices = [], vertexIndex=0
function drawImage(x, y, width, height, r, g, b, a, texX, texY, texWidth, texHeight) {
// UPDATE: Set 12 slots in vertices. Overwrite what was there last frame.
vertices[vertexIndex++] = x
vertices[vertexIndex++] = y
vertices[vertexIndex++] = width
vertices[vertexIndex++] = height
vertices[vertexIndex++] = r
vertices[vertexIndex++] = g
vertices[vertexIndex++] = b
vertices[vertexIndex++] = a
vertices[vertexIndex++] = texX
vertices[vertexIndex++] = texY
vertices[vertexIndex++] = texWidth
vertices[vertexIndex++] = texHeight
}
// 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, r, g, b, a, 1, 1, 1, 1)
}
function gameLoop() {
window.requestAnimationFrame(gameLoop)
// Draw a wide rectangle.
drawRectangle(0,370, 200,50, 200,100,10,1)
// Draw the moving image.
var x = 250+Math.sin(Date.now()*.004)*250
var y = 200
var blue = 128 + Math.floor(Math.sin(Date.now()*.01) * 127)
var frame = (Date.now()/100)&1
drawImage(
x, y,
100, 200,
0, 255, blue, 1,
frame*32,32, 32,32
)
glRender()
}
function glRender() {
// Clear the screen.
gl.clear(gl.COLOR_BUFFER_BIT)
// UPDATE: Send to webGL the section of vertices[] that we acually used this frame.
gl.bufferSubData(gl.ARRAY_BUFFER, 0, vertices.subarray(0,vertexIndex))
// Draw all the images. vertexIndex/12 is the amount of images we are drawing. We set 12 slots each drawImage().
// 6 is the amount of points per image since they are made of 2 triangles.
glExtension.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, vertexIndex/12)
// UPDATE: Reset vertex index. We will overwrite the slots every frame.
// This way you don't need to delete objects from the screen. You just stop drawing them.
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 = vec4(rgba.xyz / 255.0, rgba.w);" +
// 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())
// UPDATE: 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 * 48)
gl.bufferData(gl.ARRAY_BUFFER, arrayBuffer, gl.DYNAMIC_DRAW)
vertices = new Float32Array(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"
// We need to tell it that each vertex takes 48 bytes now (8 floats)
var attribute = gl.getAttribLocation(shaderProgram, "coordinates")
gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 48, 0)
gl.enableVertexAttribArray(attribute)
glExtension.vertexAttribDivisorANGLE(attribute, 1)
// Then read width and height from the vertex array and store them in "drawSize".
// since "coordinates" won't be getting set to every corner of the image anymore (just top-left) we'll add drawSize to it instead.
var attribute = gl.getAttribLocation(shaderProgram, "drawSize")
gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 48, 8)
gl.enableVertexAttribArray(attribute)
glExtension.vertexAttribDivisorANGLE(attribute, 1)
// Tell webGL to read 4 floats from the vertex array for each vertex
// and store them in my vec4 shader variable I've named "rgba"
// Start after 8 bytes. (After the 2 floats for x and y)
var attribute = gl.getAttribLocation(shaderProgram, "rgba")
gl.vertexAttribPointer(attribute, 4, gl.FLOAT, false, 48, 16)
gl.enableVertexAttribArray(attribute)
glExtension.vertexAttribDivisorANGLE(attribute, 1)
// Tell webGL to read 2 floats from the vertex array for each vertex
// and store them in my vec2 shader variable I've named "texPos"
var attribute = gl.getAttribLocation(shaderProgram, "texPos")
gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 48, 32)
gl.enableVertexAttribArray(attribute)
glExtension.vertexAttribDivisorANGLE(attribute, 1)
// Also read 2 slots for size of the part of the texture you are drawing.
// since "texPos" won't be getting set to every corner of the the part of our texture that we want to draw anymore (just top-left),
// we'll add texPos+texPartSize in our shader now to get to the other corners.
var attribute = gl.getAttribLocation(shaderProgram, "texPartSize")
gl.vertexAttribPointer(attribute, 2, gl.FLOAT, false, 48, 40)
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: