renderer.js

import { gl } from './common.js';
import { Camera } from "./camera.js";
import { shader, Shader } from "./shader.js";
import { VertexArray } from "./vertex_array.js";
import { VertexBuffer, IndexBuffer } from "./buffer.js";
import * as fontData from './font_data.json';
import { Texture, textures, font } from './texture.js';
import { vec2 } from 'gl-matrix';
import * as Colors from './colors.json'

/**
 * This class instance includes all rendering methods.
 * @readonly
 * @type {HB.Renderer}
 * @memberof HB
 */
let renderer = undefined;

/**
 * Class with all of the essential rendering setup and methods.
 * @readonly
 * @memberof HB
 */
class Renderer {
	/**
	 * (DO NOT USE) Internal use by Hummingbird only, all methods are available on {@link HB.renderer}.
	 * @constructor
	 * @readonly
	 * @memberof HB
	 */
	constructor() {
		gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
		gl.enable(gl.BLEND);
		gl.cullFace(gl.FRONT);
		gl.enable(gl.CULL_FACE);

		/**
		 * (DO NOT USE) This is the amount of texture units available, for increased compatibility/performance.
		 * @readonly
		 */
		this.textureUnits = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) || 8;
		/**
		 * (DO NOT USE) Internal variable to keep track of max vertices in one batch, default is 4000.
		 * @readonly
		 */
		this.maxVertexCount = 4000;
		/**
		 * (DO NOT USE) Internal variable to keep track of max indices in one batch, default is 6000.
		 * @readonly
		 */
		this.maxIndexCount = 6000;

		this.resetBatch();

		this.camera = new Camera();
	}

	/**
	 * (DO NOT USE) Internal method for creating the {@link HB.renderer} instance.
	 * @readonly
	 */
	static init() {
		renderer = new Renderer();
		Shader.init();
		VertexArray.init(renderer.maxVertexCount, renderer.maxIndexCount);
		Texture.init();
	}

	/**
	 * Method for starting a new rendering batch, is automatically called in {@link HB.internalUpdate}.
	 */
	startBatch() { this.resetBatch(); }
	/**
	 * Method for ending the rendering batch, is automatically called in {@link HB.internalUpdate}.
	 */
	endBatch() { this.flushBatch(); }

	/**
	 * (DO NOT USE) Internal method for resetting the rendering batch's variables, is automatically called by {@link HB.Renderer#startBatch}.
	 * @readonly
	 */
	resetBatch() {
		this.batchedVertexCount = 0;
		this.batchedIndexCount = 0;
		this.batchTextureIndex = 1;
		this.batchTextureCache = {};
	}

	/**
	 * Method for clearing the screen with a certain color, can only be called when a batch has been started.
	 * @param {glMatrix.vec4} color - Color to clear the screen with.
	 */
	clear(color) {
		if (color !== undefined) gl.clearColor(color[0], color[1], color[2], color[3]);
		gl.clear(gl.COLOR_BUFFER_BIT);
	}

	/**
	 * Method for drawing a colored point on screen, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} pos - Center position at which to draw the point.
	 * @param {number} size=1 - Size of the point, the point is a square defaulting to 1 pixel.
	 * @param {glMatrix.vec4} color - Color of the point.
	 */
	colorPoint(pos, size = 1, color) {
		this.flushBatchIfBufferFilled();
		const halfSize = size * 0.5;
		this.drawBatchedQuad(pos[0] - halfSize, pos[1] - halfSize, size, size, 0, color);
	}

	/**
	 * Method for drawing a colored polygon on screen, can only be called when a batch has been started.
	 * @param {Array} points - Array of {@link glMatrix.vec2}s with positions of all points.
	 * @param {glMatrix.vec4} color - Color of the polygon.
	 * @param {number} center=0 - Element of points Array which indicates the center of the polygon, only needed occasionally for concave polygons.
	 */
	colorPolygon(points, color, center = 0) {
		for (let i = 0; i < points.length - 1; i++) {
			if (i === center) continue;

			this.flushBatchIfBufferFilled(3, 3);

			this.drawBatchedTriangle(
				points[center][0], points[center][1],
				points[i][0], points[i][1],
				points[i + 1][0], points[i + 1][1],
				color
			);
		}
	}

	/**
	 * Method for drawing a colored rectangle on screen, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} pos - Top-left position of the rectangle.
	 * @param {glMatrix.vec2} size - Size of the rectangle.
	 * @param {glMatrix.vec4} color - Color of the rectangle.
	 */
	colorRectangle(pos, size, color) {
		this.flushBatchIfBufferFilled();
		this.drawBatchedQuad(pos[0], pos[1], size[0], size[1], 0, color);
	}

	/**
	 * Method for drawing a textured rectangle on screen, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} pos - Top-left position of the rectangle.
	 * @param {glMatrix.vec2} size - Size of the rectangle.
	 * @param {HB.Texture} texture - {@link HB.Texture} instance to texture with.
	 */
	textureRectangle(pos, size, texture) {
		this.flushBatchIfBufferFilled();
		this.drawBatchedQuad(pos[0], pos[1], size[0], size[1], this.getBatchTextureIndex(texture));
	}

	/**
	 * Method for drawing a rotated colored rectangle on screen, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} pos - Top-left position of the rectangle.
	 * @param {glMatrix.vec2} size - Size of the rectangle.
	 * @param {number} angle - Angle in radians with which the rectangle gets rotated around its center (pos[0]+size[0]/2, pos[1]+size[1]/2).
	 * @param {glMatrix.vec4} color - Color of the rectangle.
	 */
	rotatedColorRectangle(pos, size, angle, color) {
		this.flushBatchIfBufferFilled();
		this.drawRectangleWithRotation(pos, size, angle, 0, color);
	}

	/**
	 * Method for drawing a rotated textured rectangle on screen, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} pos - Top-left position of the rectangle.
	 * @param {glMatrix.vec2} size - Size of the rectangle.
	 * @param {number} angle - Angle in radians with which the rectangle gets rotated around its center (pos[0] + (size[0] / 2), pos[1] + (size[1] / 2)).
	 * @param {HB.Texture} texture - {@link HB.Texture} instance to texture with.
	 */
	rotatedTextureRectangle(pos, size, angle, texture) {
		this.flushBatchIfBufferFilled();
		this.drawRectangleWithRotation(pos, size, angle, this.getBatchTextureIndex(texture));
	}

	/**
	 * (DO NOT USE) Internal method for drawing a rotated rectangle on screen. Use {@link HB.Renderer#rotatedColorRectangle} or {@link HB.Renderer#rotatedTextureRectangle} instead. Can only be called when a batch has been started.
	 * @readonly
	 * @param {glMatrix.vec2} pos - Top-left position of the rectangle.
	 * @param {glMatrix.vec2} size - Size of the rectangle.
	 * @param {number} angle - Angle in radians with which the rectangle gets rotated around its center (pos[0] + (size[0] / 2), pos[1] + (size[1] / 2)).
	 * @param {HB.Texture} texture=0 - If available, {@link HB.Texture} instance to texture with.
	 * @param {glMatrix.vec4} color={@link HB.Colors.White} - If available, color of the rectangle.
	 */
	drawRectangleWithRotation(pos, size, angle, texture = 0, color = Colors.White) {
		angle = HB.Math.radians(angle);
		const cosX = size[0] * -0.5 * Math.cos(angle), cosY = size[1] * -0.5 * Math.cos(angle);
		const cosX1 = size[0] * 0.5 * Math.cos(angle), cosY1 = size[1] * 0.5 * Math.cos(angle);
		const sinX = size[0] * -0.5 * Math.sin(angle), sinY = size[1] * -0.5 * Math.sin(angle);
		const sinX1 = size[0] * 0.5 * Math.sin(angle), sinY1 = size[1] * 0.5 * Math.sin(angle);

		this.drawArbitraryBatchedQuad(
			cosX - sinY + pos[0] + size[0] / 2, sinX + cosY + pos[1] + size[1] / 2,
			cosX1 - sinY + pos[0] + size[0] / 2, sinX1 + cosY + pos[1] + size[1] / 2,
			cosX1 - sinY1 + pos[0] + size[0] / 2, sinX1 + cosY1 + pos[1] + size[1] / 2,
			cosX - sinY1 + pos[0] + size[0] / 2, sinX + cosY1 + pos[1] + size[1] / 2,
			texture, color
		);
	}

	/**
	 * Method for drawing a colored line on screen from one point to another, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} vectorA - First point of the line.
	 * @param {glMatrix.vec2} vectorB - Second point of the line.
	 * @param {number} thickness - The thickness of the line in pixels.
	 * @param {glMatrix.vec4} color - Color of the rectangle.
	 */
	colorLine(vectorA, vectorB, thickness, color) {
		const angle0 = Math.atan2(vectorB[1] - vectorA[1], vectorB[0] - vectorA[0]);
		let angleA = vec2.fromValues(thickness * 0.5, 0);
		vec2.rotate(angleA, angleA, vec2.create(), angle0 - Math.PI * 0.5);
		let angleB = vec2.fromValues(thickness * 0.5, 0);
		vec2.rotate(angleB, angleB, vec2.create(), angle0 + Math.PI * 0.5);

		this.flushBatchIfBufferFilled();

		this.drawArbitraryBatchedQuad(
			vectorA[0] - angleA[0], vectorA[1] - angleA[1],
			vectorA[0] + angleA[0], vectorA[1] + angleA[1],
			vectorB[0] - angleB[0], vectorB[1] - angleB[1],
			vectorB[0] + angleB[0], vectorB[1] + angleB[1],
			0, color
		);
	}

	/**
	 * Method for drawing a colored ellipse on screen, can only be called when a batch has been started.
	 * @param {glMatrix.vec2} pos - Top-left position of the ellipse.
	 * @param {glMatrix.vec2} size - Size of the ellipse's individual axes.
	 * @param {glMatrix.vec4} color - Color of the ellipse.
	 */
	colorEllipse(pos, size, color) {
		this.flushBatchIfBufferFilled();
		this.drawBatchedQuad(pos[0], pos[1], size[0], size[1], this.getBatchTextureIndex(textures.Hummingbird_Circle), color);
	}

	/**
	 * Method for drawing colored text on screen, can only be called when a batch has been started.
	 * @param {string} string - ASCII text to be rendered, see the charset in {@link https://projects.brandond.nl/Hummingbird/assets/arial_pretty.json} for all characters.
	 * @param {glMatrix.vec2} pos - Position of the text, anchor-point determined by the align parameter.
	 * @param {number} size=12 - Pixel size (height) of the text.
	 * @param {string} align="start-start" - Where to place the anchor-point, this is a string formatted as "'x-anchor'-'y-anchor'", e.g. "end-center". Possible values are [start, center, end], modelled after [this]{@link https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/textAlign#options}.
	 * @param {glMatrix.vec4} color - Color of the text.
	 * @returns {number} Final width of the rendered text.
	 */
	colorText(string, pos, size = 12, align = 'start-start', color) {
		const alignTo = align.split('-');

		const glyphs = [], kernings = {};
		const scalar = size / fontData.info.size;
		let width = 0;
		const height = fontData.common.lineh * scalar;

		let prevKerns;
		for (let i = 0; i < string.length; i++) {
			const glyph = fontData.chars[string[i]] || fontData.chars['?'];
			width += glyph.xadv * scalar;
			glyphs.push(glyph);

			if (prevKerns !== undefined) {
				const kerning = prevKerns[glyph.id];
				if (kerning !== undefined) {
					width += kerning * scalar;
					kernings[glyph.id] = kerning;
				}
			}
			prevKerns = glyph.kerns;
		}

		let offsetx = 0, offsety = 0;
		switch (alignTo[0]) {
			case 'start': break;
			case 'center': offsetx = -width / 2; break;
			case 'end': offsetx = -width; break;
		}
		switch (alignTo[1]) {
			case 'start': offsety = -fontData.info.padding[3] * 0.5; break;
			case 'center': offsety = -height / 2; break;
			case 'end': offsety = -height; break;
		}

		let textureIndex = this.getBatchTextureIndex(font);
		for (let glyph of glyphs) {
			const kerning = kernings[glyph.id];
			if (kerning !== undefined) offsetx += kerning * scalar;

			if (this.flushBatchIfBufferFilled()) textureIndex = this.getBatchTextureIndex(font);

			this.drawBatchedQuad(
				pos[0] + glyph.xoff * scalar + offsetx, pos[1] + glyph.yoff * scalar + offsety,
				glyph.w * scalar, glyph.h * scalar,
				textureIndex, color, scalar * fontData.distanceField.distanceRange,
				glyph.x / fontData.common.scaleW, glyph.y / fontData.common.scaleH,
				glyph.w / fontData.common.scaleW, glyph.h / fontData.common.scaleH
			);

			offsetx += glyph.xadv * scalar;
		}

		return width;
	}

	/**
	 * (DO NOT USE) Internal method for drawing a triangle on screen. Use {@link HB.Renderer#colorPolygon} instead. Can only be called when a batch has been started.
	 * @readonly
	 * @param {number} x1 - X-coordinate of the first point.
	 * @param {number} y1 - Y-coordinate of the first point.
	 * @param {number} x2 - X-coordinate of the second point.
	 * @param {number} y2 - Y-coordinate of the second point.
	 * @param {number} x3 - X-coordinate of the third point.
	 * @param {number} y3 - Y-coordinate of the third point.
	 * @param {glMatrix.vec4} color - Color of the triangle.
	 */
	drawBatchedTriangle(x1, y1, x2, y2, x3, y3, color) {
		const start = this.batchedVertexCount * VertexArray.layout.stride;
		VertexBuffer.data[start] = x1;
		VertexBuffer.data[start + 1] = y1;
		VertexBuffer.data[start + 2] = color[0];
		VertexBuffer.data[start + 3] = color[1];
		VertexBuffer.data[start + 4] = color[2];
		VertexBuffer.data[start + 5] = color[3];
		VertexBuffer.data[start + 6] = 0;
		VertexBuffer.data[start + 7] = 1;
		VertexBuffer.data[start + 8] = 0;
		VertexBuffer.data[start + 9] = 0;

		VertexBuffer.data[start + 10] = x2;
		VertexBuffer.data[start + 11] = y2;
		VertexBuffer.data[start + 12] = color[0];
		VertexBuffer.data[start + 13] = color[1];
		VertexBuffer.data[start + 14] = color[2];
		VertexBuffer.data[start + 15] = color[3];
		VertexBuffer.data[start + 16] = 0.5;
		VertexBuffer.data[start + 17] = 0.5;
		VertexBuffer.data[start + 18] = 0;
		VertexBuffer.data[start + 19] = 0;

		VertexBuffer.data[start + 20] = x3;
		VertexBuffer.data[start + 21] = y3;
		VertexBuffer.data[start + 22] = color[0];
		VertexBuffer.data[start + 23] = color[1];
		VertexBuffer.data[start + 24] = color[2];
		VertexBuffer.data[start + 25] = color[3];
		VertexBuffer.data[start + 26] = 1;
		VertexBuffer.data[start + 27] = 1;
		VertexBuffer.data[start + 28] = 0;
		VertexBuffer.data[start + 29] = 0;

		IndexBuffer.data[this.batchedIndexCount] = this.batchedVertexCount;
		IndexBuffer.data[this.batchedIndexCount + 1] = this.batchedVertexCount + 1;
		IndexBuffer.data[this.batchedIndexCount + 2] = this.batchedVertexCount + 2;

		this.batchedVertexCount += 3, this.batchedIndexCount += 3;
	}

	/**
	 * (DO NOT USE) Internal method for drawing a quad on screen. Use any of the quad methods instead ({@link HB.Renderer#colorRectangle}, {@link HB.Renderer#textureRectangle}, etc.). Can only be called when a batch has been started.
	 * @readonly
	 * @param {number} x - X-coordinate of the quad.
	 * @param {number} y - Y-coordinate of the quad.
	 * @param {number} w - Width of the quad.
	 * @param {number} h - Height of the quad.
	 * @param {number} tex - Optional internal batch-specific texture ID.
	 * @param {glMatrix.vec4} col - Optional color of the quad.
	 * @param {number} textRange - Optional value to check whether text is being rendered and anti-alias it.
	 * @param {number} sx - UV x-coordinate of the texture, 0-1.
	 * @param {number} sy - UV y-coordinate of the texture, 0-1.
	 * @param {number} sw - UV width of the texture, 0-1.
	 * @param {number} sh - UV height of the texture, 0-1.
	 */
	drawBatchedQuad(x, y, w, h, tex, col, textRange, sx, sy, sw, sh) {
		this.drawArbitraryBatchedQuad(
			x, y,
			x + w, y,
			x + w, y + h,
			x, y + h,
			tex, col, textRange,
			sx, sy,
			sw, sh
		)
	}

	/**
	 * (DO NOT USE) Internal, most basic method for drawing a quad on screen. Use any of the quad methods instead ({@link HB.Renderer#colorRectangle}, {@link HB.Renderer#textureRectangle}, etc.). Can only be called when a batch has been started.
	 * @readonly
	 * @param {number} x0 - Top-left x-coordinate.
	 * @param {number} y0 - Top-left y-coordinate.
	 * @param {number} x1 - Top-right x-coordinate.
	 * @param {number} y1 - Top-right y-coordinate.
	 * @param {number} x2 - Bottom-right x-coordinate.
	 * @param {number} y2 - Bottom-right y-coordinate.
	 * @param {number} x3 - Bottom-left x-coordinate.
	 * @param {number} y3 - Bottom-left y-coordinate.
	 * @param {number} tex=0 - Optional internal batch-specific texture ID.
	 * @param {glMatrix.vec4} col={@link HB.Colors.White} - Optional color of the quad.
	 * @param {number} textRange=0 - Optional value to check whether text is being rendered and anti-alias it.
	 * @param {number} sx=0 - UV x-coordinate of the texture, 0-1.
	 * @param {number} sy=0 - UV y-coordinate of the texture, 0-1.
	 * @param {number} sw=1 - UV width of the texture, 0-1.
	 * @param {number} sh=1 - UV height of the texture, 0-1.
	 */
	drawArbitraryBatchedQuad(x0, y0, x1, y1, x2, y2, x3, y3, tex = 0, col = Colors.White, textRange = 0, sx = 0, sy = 0, sw = 1, sh = 1) {
		const start = this.batchedVertexCount * VertexArray.layout.stride;
		VertexBuffer.data[start] = x0;
		VertexBuffer.data[start + 1] = y0;
		VertexBuffer.data[start + 2] = col[0];
		VertexBuffer.data[start + 3] = col[1];
		VertexBuffer.data[start + 4] = col[2];
		VertexBuffer.data[start + 5] = col[3];
		VertexBuffer.data[start + 6] = sx;
		VertexBuffer.data[start + 7] = sy;
		VertexBuffer.data[start + 8] = tex;
		VertexBuffer.data[start + 9] = textRange;

		VertexBuffer.data[start + 10] = x1;
		VertexBuffer.data[start + 11] = y1;
		VertexBuffer.data[start + 12] = col[0];
		VertexBuffer.data[start + 13] = col[1];
		VertexBuffer.data[start + 14] = col[2];
		VertexBuffer.data[start + 15] = col[3];
		VertexBuffer.data[start + 16] = sx + sw;
		VertexBuffer.data[start + 17] = sy;
		VertexBuffer.data[start + 18] = tex;
		VertexBuffer.data[start + 19] = textRange;

		VertexBuffer.data[start + 20] = x2;
		VertexBuffer.data[start + 21] = y2;
		VertexBuffer.data[start + 22] = col[0];
		VertexBuffer.data[start + 23] = col[1];
		VertexBuffer.data[start + 24] = col[2];
		VertexBuffer.data[start + 25] = col[3];
		VertexBuffer.data[start + 26] = sx + sw;
		VertexBuffer.data[start + 27] = sy + sh;
		VertexBuffer.data[start + 28] = tex;
		VertexBuffer.data[start + 29] = textRange;

		VertexBuffer.data[start + 30] = x3;
		VertexBuffer.data[start + 31] = y3;
		VertexBuffer.data[start + 32] = col[0];
		VertexBuffer.data[start + 33] = col[1];
		VertexBuffer.data[start + 34] = col[2];
		VertexBuffer.data[start + 35] = col[3];
		VertexBuffer.data[start + 36] = sx;
		VertexBuffer.data[start + 37] = sy + sh;
		VertexBuffer.data[start + 38] = tex;
		VertexBuffer.data[start + 39] = textRange;

		IndexBuffer.data[this.batchedIndexCount] = this.batchedVertexCount;
		IndexBuffer.data[this.batchedIndexCount + 1] = this.batchedVertexCount + 1;
		IndexBuffer.data[this.batchedIndexCount + 2] = this.batchedVertexCount + 2;
		IndexBuffer.data[this.batchedIndexCount + 3] = this.batchedVertexCount + 2;
		IndexBuffer.data[this.batchedIndexCount + 4] = this.batchedVertexCount + 3;
		IndexBuffer.data[this.batchedIndexCount + 5] = this.batchedVertexCount;

		this.batchedVertexCount += 4, this.batchedIndexCount += 6;
	}

	/**
	 * (DO NOT USE) Internal method to get a batch-specific texture ID. Can only be called when a batch has been started.
	 * @param {HB.Texture} texture - {@link HB.Texture} instance to texture with.
	 * @returns {number} Internal batch-specific texture ID.
	 */
	getBatchTextureIndex(texture) {
		let textureIndex = this.batchTextureCache[texture.name];
		if (textureIndex === undefined) {
			textureIndex = this.batchTextureIndex;
			texture.bind(textureIndex);
			this.batchTextureCache[texture.name] = textureIndex;

			this.batchTextureIndex += 1;
			if (this.batchTextureIndex > this.textureUnits)
				this.flushBatch();
		}
		return textureIndex;
	}

	/**
	 * (DO NOT USE) Internal method to test whether the batch has to rendered to the screen, this happens whenever either the Vertex- or IndexBuffer is filled. Can only be called when a batch has been started.
	 * @param {number} vertices - Amount of vertices to add to buffer for check.
	 * @param {number} indices - Amount of indices to add to buffer for check.
	 * @returns {boolean} Whether the batch had to be rendered.
	 */
	flushBatchIfBufferFilled(vertices = 4, indices = 6) {
		if ((this.batchedVertexCount + vertices) >= this.maxVertexCount || (this.batchedIndexCount + indices) >= this.maxIndexCount) {
			this.flushBatch();
			return true;
		}
		return false;
	}

	/**
	 * (DO NOT USE) Internal method for rendering the current batch's contents, is automatically called by {@link HB.Renderer#endBatch} or {@link HB.Renderer#flushBatchIfBufferFilled}.
	 * @readonly
	 */
	flushBatch() {
		VertexBuffer.write(this.batchedVertexCount * VertexArray.layout.stride);
		IndexBuffer.write(this.batchedIndexCount);
		this.drawIndexedTriangles(this.batchedIndexCount);
		this.resetBatch();
	}

	/**
	 * (DO NOT USE) Internal method with the final draw call, is automatically called by {@link HB.Renderer#flushBatch}.
	 * @readonly
	 * @param {number} indexCount - Amount of indices to be rendered.
	 */
	drawIndexedTriangles(indexCount) {
		shader.bind();

		gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0);
	}

	/**
	 * (DO NOT USE) Internal method for cleaning up all renderer related objects (textures, shader, buffers, etc.), is automatically called when the window is closed.
	 * @readonly
	 */
	delete() {
		for (let tex in textures) textures[tex].delete();
		shader.delete();
		VertexArray.delete();
		VertexBuffer.delete();
		IndexBuffer.delete();
	}
}

export {
	renderer,
	Renderer
};