284 lines
8.0 KiB
TypeScript
284 lines
8.0 KiB
TypeScript
import * as Assets from "./assets.js";
|
|
import {Vec2, Vec3, Vec4} from "./common.js";
|
|
|
|
export function fullscreenCanvas(gfx: Graphics, id: string) {
|
|
const canvas = document.getElementById(id) as HTMLCanvasElement;
|
|
canvas.width = window.innerWidth;
|
|
canvas.height = window.innerHeight;
|
|
gfx.ctx.viewport(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
function createShader(ctx: WebGL2RenderingContext, type: GLenum, source: string): WebGLShader {
|
|
var shader = ctx.createShader(type);
|
|
if (!shader) {
|
|
throw new Error("Couldn't create shader: " + type);
|
|
}
|
|
|
|
ctx.shaderSource(shader, source);
|
|
ctx.compileShader(shader);
|
|
|
|
var success = ctx.getShaderParameter(shader, ctx.COMPILE_STATUS);
|
|
if (!success) {
|
|
console.log(ctx.getShaderInfoLog(shader));
|
|
ctx.deleteShader(shader);
|
|
throw new Error("Couldn't compile shader: " + type);
|
|
}
|
|
|
|
return shader;
|
|
}
|
|
|
|
function createProgram(ctx: WebGL2RenderingContext, vertexShaderSource: string | null, fragmentShaderSource: string | null): WebGLProgram {
|
|
var program = ctx.createProgram();
|
|
|
|
if (vertexShaderSource !== null) {
|
|
const vs = createShader(ctx, ctx.VERTEX_SHADER, vertexShaderSource);
|
|
ctx.attachShader(program, vs);
|
|
}
|
|
|
|
if (fragmentShaderSource !== null) {
|
|
const fs = createShader(ctx, ctx.FRAGMENT_SHADER, fragmentShaderSource);
|
|
ctx.attachShader(program, fs);
|
|
}
|
|
|
|
ctx.linkProgram(program);
|
|
|
|
var success = ctx.getProgramParameter(program, ctx.LINK_STATUS);
|
|
if (!success) {
|
|
console.log(ctx.getProgramInfoLog(program));
|
|
ctx.deleteProgram(program);
|
|
throw new Error("Failed to create program!");
|
|
}
|
|
|
|
return program;
|
|
}
|
|
|
|
export enum DrawTag {
|
|
ISO,
|
|
}
|
|
|
|
export interface Drawable {
|
|
fill: Texture | Assets.Color;
|
|
attribs: string[];
|
|
tags: Array<DrawTag>;
|
|
|
|
data: Array<number>;
|
|
vertexStride: number;
|
|
stride: number;
|
|
}
|
|
|
|
export class Graphics {
|
|
ctx: WebGL2RenderingContext;
|
|
program: WebGLProgram;
|
|
attribs: Map<string, Attribute> = new Map();
|
|
uniforms: Map<string, WebGLUniformLocation> = new Map();
|
|
vao: WebGLVertexArrayObject;
|
|
vbo: WebGLBuffer;
|
|
|
|
toRender: Array<Drawable> = [];
|
|
texCount: number = 0;
|
|
|
|
constructor(ctx: WebGL2RenderingContext, vs: string, fs: string) {
|
|
this.ctx = ctx;
|
|
this.program = createProgram(ctx, vs, fs);
|
|
this.vao = ctx.createVertexArray();
|
|
this.vbo = ctx.createBuffer();
|
|
|
|
ctx.bindVertexArray(this.vao);
|
|
ctx.viewport(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
ctx.useProgram(this.program);
|
|
|
|
ctx.blendFunc(ctx.SRC_ALPHA, ctx.ONE_MINUS_SRC_ALPHA);
|
|
ctx.enable(ctx.BLEND);
|
|
}
|
|
|
|
clear(r: number, g: number, b: number, a: number) {
|
|
this.ctx.clearColor(r, g, b, a);
|
|
this.ctx.clear(this.ctx.COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
createAttribute(name: string): Attribute {
|
|
const attrib = new Attribute(this.ctx, this.program, name);
|
|
this.attribs.set(name, attrib);
|
|
return attrib;
|
|
}
|
|
|
|
getAttribute(name: string): Attribute {
|
|
const attrib = this.attribs.get(name);
|
|
|
|
if (attrib === undefined)
|
|
throw new Error("Tried to get uninitialized attribute: " + name);
|
|
|
|
return attrib;
|
|
}
|
|
|
|
createUniform(name: string) {
|
|
const loc = this.ctx.getUniformLocation(this.program, name);
|
|
|
|
if (loc === null)
|
|
throw new Error("Couldn't get location for uniform: " + name);
|
|
|
|
this.uniforms.set(name, loc);
|
|
}
|
|
|
|
getUniform(name: string): WebGLUniformLocation {
|
|
const loc = this.uniforms.get(name);
|
|
|
|
if (loc === undefined)
|
|
throw new Error("Tried to get uninitialized uniform: " + name);
|
|
|
|
return loc;
|
|
}
|
|
|
|
draw() {
|
|
for (let o of this.toRender) {
|
|
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.vbo);
|
|
this.ctx.bufferData(this.ctx.ARRAY_BUFFER, new Float32Array(o.data), this.ctx.STATIC_DRAW);
|
|
|
|
let aid = 0;
|
|
for (let a of o.attribs) {
|
|
let attr = this.getAttribute(a);
|
|
if (!attr.formatted)
|
|
throw new Error("Tried to use unformatted attribute!");
|
|
|
|
this.ctx.enableVertexAttribArray(attr.loc);
|
|
this.ctx.vertexAttribPointer(attr.loc, attr.size, attr.type, attr.normalized, o.stride, aid * 4);
|
|
aid += attr.size;
|
|
}
|
|
|
|
// Generalize the tag uniforms aka. don't hard code them
|
|
for (let t of o.tags) {
|
|
switch (t) {
|
|
case DrawTag.ISO: {
|
|
this.ctx.uniform1ui(this.getUniform("u_isIso"), 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (o.fill instanceof Texture)
|
|
{
|
|
this.ctx.uniform1i(this.getUniform("u_isTex"), 1);
|
|
o.fill.bind(this);
|
|
}
|
|
|
|
this.ctx.drawArrays(this.ctx.TRIANGLES, 0, o.data.length / o.vertexStride);
|
|
|
|
if (o.fill instanceof Texture)
|
|
{
|
|
this.ctx.uniform1i(this.getUniform("u_isTex"), 0);
|
|
o.fill.unbind(this);
|
|
}
|
|
|
|
for (let t of o.tags) {
|
|
switch (t) {
|
|
case DrawTag.ISO: {
|
|
this.ctx.uniform1ui(this.getUniform("u_isIso"), 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// TODO: Maybe add persistent rendering?
|
|
this.toRender = [];
|
|
}
|
|
}
|
|
|
|
export class Attribute {
|
|
loc: GLint;
|
|
formatted: boolean = false;
|
|
|
|
size: GLint = 0;
|
|
type: GLenum = 0;
|
|
offset: GLintptr = 0;
|
|
normalized: GLboolean = false;
|
|
|
|
constructor(ctx: WebGL2RenderingContext, program: WebGLProgram, name: string) {
|
|
this.loc = ctx.getAttribLocation(program, name);
|
|
}
|
|
|
|
format(
|
|
size: GLint,
|
|
type: GLenum,
|
|
normalized: GLboolean,
|
|
offset: GLintptr)
|
|
{
|
|
this.size = size;
|
|
this.type = type;
|
|
this.normalized = normalized;
|
|
this.offset = offset;
|
|
|
|
this.formatted = true;
|
|
}
|
|
}
|
|
|
|
export class Texture {
|
|
tex: WebGLTexture | null;
|
|
texId: number;
|
|
width: number = 0;
|
|
height: number = 0;
|
|
|
|
constructor(texId: number, tex: WebGLTexture, width: number, height: number) {
|
|
this.height = height;
|
|
this.width = width;
|
|
this.tex = tex;
|
|
this.texId = texId;
|
|
}
|
|
|
|
// TODO: Load sprite sheet only once
|
|
// TODO: Allow changing sprite size
|
|
static async load(gfx: Graphics, path: string): Promise<Texture> {
|
|
let image = await loadTexture(path);
|
|
|
|
let tex = gfx.ctx.createTexture();
|
|
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, tex);
|
|
gfx.ctx.texImage2D(gfx.ctx.TEXTURE_2D, 0, gfx.ctx.RGBA, gfx.ctx.RGBA, gfx.ctx.UNSIGNED_BYTE, image);
|
|
gfx.ctx.texParameteri(gfx.ctx.TEXTURE_2D, gfx.ctx.TEXTURE_MAG_FILTER, gfx.ctx.NEAREST);
|
|
gfx.ctx.texParameteri(gfx.ctx.TEXTURE_2D, gfx.ctx.TEXTURE_MIN_FILTER, gfx.ctx.NEAREST);
|
|
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, null);
|
|
|
|
gfx.texCount += 1;
|
|
return new Texture(gfx.texCount, tex, image.width, image.height);
|
|
}
|
|
|
|
bind(gfx: Graphics) {
|
|
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, this.tex);
|
|
}
|
|
|
|
unbind(gfx: Graphics) {
|
|
gfx.ctx.bindTexture(gfx.ctx.TEXTURE_2D, null);
|
|
}
|
|
}
|
|
|
|
async function loadTexture(path: string): Promise<HTMLImageElement> {
|
|
return new Promise(resolve => {
|
|
const img = new Image();
|
|
img.addEventListener("load", () => {
|
|
resolve(img);
|
|
})
|
|
img.src = path;
|
|
});
|
|
}
|
|
|
|
export class Camera {
|
|
dt: number = 0;
|
|
position: Vec3;
|
|
movement: Vec4;
|
|
speed: number = 600;
|
|
|
|
constructor(position: Vec3) {
|
|
this.position = position;
|
|
this.movement = new Vec4(0, 0, 0, 0);
|
|
}
|
|
|
|
update(dt: number) {
|
|
this.dt = dt;
|
|
|
|
let newPosition = this.movement.multScalarNew(this.dt);
|
|
this.position.x += (newPosition.x + newPosition.y) * this.speed;
|
|
this.position.y += (newPosition.z + newPosition.w) * this.speed;
|
|
}
|
|
}
|
|
|