I’ve made a game using the Canvas element and with 300+ images being drawn every frame I am forced to make it run at <20 fps and it takes 60+% CPU:
So I guess my best option is to switch to WebGL. I’ve been scavenging the web, looking for tutorials and examples. It’s kinda complex stuff but I managed to hack together a working source.
<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WebGL - 2D Image</title>
<link type="text/css" href="http://games.greggman.com/downloads/examples/webgl/resources/webgl-tutorials.css" rel="stylesheet" />
<script type="text/javascript" src="http://games.greggman.com/downloads/examples/webgl/resources/webgl-utils.js"></script>
<script>
window.onload = main;
function main() {
image = new Image();
image.src = "http://games.greggman.com/downloads/examples/webgl/resources/leaves.jpg"; // MUST BE SAME DOMAIN!!!
image.onload = function() {
setUp();
setInterval(function(){
x1 += 1;
y1 += 1
for (i = 0; i < 30; i++) {
for (e = 0; e < 10; e++) {
//render(image, x1+i*5, y1+e*5);
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set a rectangle the same size as the image.
setRectangle(gl, x1+i*5, y1+e*5, image.width, image.height);
// Draw the rectangle.
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
}
},1000/60);
}
}
var x1 = 0;
var y1 = 0;
var image;
var canvas;
var gl;
var texture;
function setUp() {
canvas = document.getElementById("canvas");
gl = getWebGLContext(canvas);
// setup GLSL program
vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);
// lookup uniforms
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
// set the resolution
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);
// Create a texture.
texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the parameters so we can render any size image.
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// Upload the image into the texture.
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");
var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
// provide texture coordinates for the rectangle.
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
// Create a buffer for the position of the rectangle corners.
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
}
function render(image, x, y) {
}
function randomInt(range) {
return Math.floor(Math.random() * range);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
x1, y1,
x2, y1,
x1, y2,
x1, y2,
x2, y1,
x2, y2]), gl.STATIC_DRAW);
}
</script>
<!-- vertex shader -->
<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;
attribute vec2 a_texCoord;
uniform vec2 u_resolution;
varying vec2 v_texCoord;
void main() {
// convert the rectangle from pixels to 0.0 to 1.0
vec2 zeroToOne = a_position / u_resolution;
// convert from 0->1 to 0->2
vec2 zeroToTwo = zeroToOne * 2.0;
// convert from 0->2 to -1->+1 (clipspace)
vec2 clipSpace = zeroToTwo - 1.0;
gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);
// pass the texCoord to the fragment shader
// The GPU will interpolate this value between points.
v_texCoord = a_texCoord;
}
</script></script>
<!-- fragment shader -->
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;
// our texture
uniform sampler2D u_image;
// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>
</head>
<body>
<canvas id="canvas" width="400" height="300"></canvas>
</body>
</html>
It draws 300 images but takes only 1/2 the CPU of using the normal canvas. Now that’s not too bad but it’s still not good enough.
Am I doing something wrong or unnecessary? Is there a way I can ease of the CPU by doing calculations in the GPU or caching them?
I’ve researched in shaders and would it be possible to use a shader to store textures and coordinates and just input an array to the shader to set location points of each image I need to draw?
Thanks,
I hope there’s an easy solution to this madness.
Do you really need to draw 300 (different) Images? Or just 300 Rectangles with the same texture? (this is what you are currently doing)
In general your code is very inefficent because of multiple reasons:
setRectangle())glBindTexture). Its better to setup textures once and than draw a batch of objects with the same texturesA bit more optimized version of you code would look like:
For the case that you really want to draw each object with another texture it is a bit more complicated. But again you have multiple options to optimze: