Here's what's worked for me so far:
In the test15 vertex shader, I have uniforms for transforming vertexes from model coords to world coords, and from
world coords to view coords.
I also transform from model color to final pixel color, so the same model can be rendered with different colors.
There's one more uniform, the player position, which was just for a fun effect - altering every pixel color based
on where the player is in the world.
function onProgramCreated() {
// Cache all the shader uniforms.
uViewTranslation = gl.getUniformLocation(program, 'uViewTranslation');
uViewScale = gl.getUniformLocation(program, 'uViewScale');
uModelTranslation = gl.getUniformLocation(program, 'uModelTranslation');
uModelScale = gl.getUniformLocation(program, 'uModelScale');
uModelColor = gl.getUniformLocation(program, 'uModelColor');
uPlayerPos = gl.getUniformLocation(program, 'uPlayerPos');
// Cache and enable the vertex position and color attributes.
aVertexPosition = gl.getAttribLocation(program, 'aVertexPosition');
gl.enableVertexAttribArray(aVertexPosition);
aVertexColor = gl.getAttribLocation(program, 'aVertexColor');
gl.enableVertexAttribArray(aVertexColor);
initWorld();
loop();
}
The map vertex positions are already in world-coordinates, so when I draw them, uModelScale is always (1, 1, 1) and the uModelTranslation is (0, 0). And they're already colored with near-final colors, so the uModelColor, which scales the color in my shader, is always (1, 1, 1).
Then I feed those arrays to the GL program as buffers, and cache pointers to
each of those two buffers, for later use.
function initMapAndBackgroundVertexes() {
var bgPositions = [];
var bgColors = [];
...Fill the arrays with vertex coordinates and color RBG values, as I generate the level walls...
// Send the arrays to the GL program, and cache the locations of those buffers for later.
bgPosBuff = createStaticGlBuff(bgPositions);
bgColorBuff = createStaticGlBuff(bgColors);
}
function createStaticGlBuff(values) {
var buff = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buff);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(values), gl.STATIC_DRAW);
return buff;
}
function initModelVertexes() {
// template for rectangles
var vertPositions = [];
var vertColors = [];
addRect(vertPositions, vertColors,
0, 0, -1, // x y z
1, 1, // rx ry
1, 1, 1); // r g b
rectPosBuff = createStaticGlBuff(vertPositions);
rectColorBuff = createStaticGlBuff(vertColors);
// template for circles
vertPositions.length = 0;
vertColors.length = 0;
addCircle(vertPositions, vertColors,
0, 0, -1, // x y z
1, // radius
CIRCLE_CORNERS,
1, 1, 1); // r g b
circlePosBuffs[CIRCLE_CORNERS] = createStaticGlBuff(vertPositions);
circleColorBuffs[CIRCLE_CORNERS] = createStaticGlBuff(vertColors);
}
I overbuilt the circle code to allow for multiple models, since I might want a higher polycount
for a big circle, but for now there's just one circle model.
All we have to do here is to
set the view transformations,
tell GL to draw the map vertexes,
and use model vertexes to stamp a bunch of sprites with different locations, sizes, and colors onto the canvas.
For fun, I vary every pixel's color based on the player's location, but since the GL hardware is doing the heavy
lifting, it's super fast.
function drawScene() {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
// Center the view on the player.
readPlayerPos();
viewTranslation[0] = -playerPos.x - 0.25 - playerBody.vel.x / 10;
viewTranslation[1] = -playerPos.y - 0.25 - playerBody.vel.y / 10;
gl.uniform3fv(uViewTranslation, viewTranslation);
// Remember the player's position, for tweaking the colors.
array3[0] = playerPos.x;
array3[1] = playerPos.y;
array3[2] = 0;
gl.uniform3fv(uPlayerPos, array3);
// Scale the view to encompass a fixed-size square around the player's position.
var edgeLength = Math.min(canvas.width, canvas.height);
viewScale[0] = ZOOM * edgeLength / canvas.width;
viewScale[1] = ZOOM * edgeLength / canvas.height;
gl.uniform3fv(uViewScale, viewScale);
gl.uniform3fv(uPlayerPos, [playerPos.x, playerPos.y, 0]);
// Draw the whole background.
// All the vertex data is already in the program, in bgColorBuff and bgPosBuff.
// Since the map is already in world-coordinates and world-colors,
// set all the model-to-world uniforms to do nothing.
gl.uniform3fv(uModelScale, IDENTITY_3);
gl.uniform3fv(uModelTranslation, ZERO_3);
gl.uniform3fv(uModelColor, IDENTITY_3);
drawTriangles(gl, bgPosBuff, bgColorBuff, bgTriangleCount);
// foreground
for (var id in world.bodies) {
var b = world.bodies[id];
if (b && b.mass != Infinity) {
drawBody(b);
}
}
}
function drawBody(b) {
b.getPosAtTime(world.now, bodyPos);
array3[0] = bodyPos.x;
array3[1] = bodyPos.y;
array3[2] = 0;
gl.uniform3fv(uModelTranslation, array3);
if (b.id == playerSpirit.bodyId) {
gl.uniform3fv(uModelColor, PLAYER_COLOR_3);
} else if (b.id == raySpirit.bodyId) {
gl.uniform3fv(uModelColor, RAY_SPIRIT_COLOR_3);
} else if (world.spirits[world.bodies[b.id].spiritId] instanceof BulletSpirit) {
gl.uniform3fv(uModelColor, BULLET_COLOR_3);
} else {
gl.uniform3fv(uModelColor, OTHER_COLOR_3);
}
if (b.shape === Body.Shape.RECT) {
array3[0] = b.rectRad.x;
array3[1] = b.rectRad.y;
array3[2] = 1;
gl.uniform3fv(uModelScale, array3);
drawTriangles(gl, rectPosBuff, rectColorBuff, 2);
} else if (b.shape === Body.Shape.CIRCLE) {
array3[0] = b.rad;
array3[1] = b.rad;
array3[2] = 1;
gl.uniform3fv(uModelScale, array3);
drawTriangleFan(gl, circlePosBuffs[CIRCLE_CORNERS], circleColorBuffs[CIRCLE_CORNERS], CIRCLE_CORNERS);
}
}
function drawTriangles(gl, positionBuff, colorBuff, triangleCount) {
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuff);
gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuff);
gl.vertexAttribPointer(aVertexColor, 4, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLES, 0, triangleCount * 3);
}
function drawTriangleFan(gl, positionBuff, colorBuff, cornerCount) {
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuff);
gl.vertexAttribPointer(aVertexPosition, 3, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, colorBuff);
gl.vertexAttribPointer(aVertexColor, 4, gl.FLOAT, false, 0, 0);
gl.drawArrays(gl.TRIANGLE_FAN, 0, cornerCount + 2);
}
The live code is at /test15/main.js, and it's on GitHub.