added webGL support

This commit is contained in:
Joe Weaver 2021-02-15 19:44:47 -05:00
parent eeaf73bab4
commit 4214ef7fd4
43 changed files with 1841 additions and 40 deletions

6
.gitignore vendored
View File

@ -7,6 +7,12 @@ dist/*
# Include the demo_assets folder # Include the demo_assets folder
!dist/demo_assets/ !dist/demo_assets/
# Include the built-in asset folder
!dist/builtin/
# Include the hw1 assets
!dist/hw1_assets/
### IF YOU ARE MAKING A PROJECT, YOU MAY WANT TO UNCOMMENT THIS LINE ### ### IF YOU ARE MAKING A PROJECT, YOU MAY WANT TO UNCOMMENT THIS LINE ###
# !dist/assets/ # !dist/assets/

7
dist/builtin/shaders/point.fshader vendored Normal file
View File

@ -0,0 +1,7 @@
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}

8
dist/builtin/shaders/point.vshader vendored Normal file
View File

@ -0,0 +1,8 @@
attribute vec4 a_Position;
uniform float u_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = u_PointSize;
}

7
dist/builtin/shaders/rect.fshader vendored Normal file
View File

@ -0,0 +1,7 @@
precision mediump float;
uniform vec4 u_Color;
void main(){
gl_FragColor = u_Color;
}

7
dist/builtin/shaders/rect.vshader vendored Normal file
View File

@ -0,0 +1,7 @@
attribute vec4 a_Position;
uniform mat4 u_Transform;
void main(){
gl_Position = u_Transform * a_Position;
}

9
dist/builtin/shaders/sprite.fshader vendored Normal file
View File

@ -0,0 +1,9 @@
precision mediump float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main(){
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}

13
dist/builtin/shaders/sprite.vshader vendored Normal file
View File

@ -0,0 +1,13 @@
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
uniform mat4 u_Transform;
uniform vec2 u_texShift;
uniform vec2 u_texScale;
varying vec2 v_TexCoord;
void main(){
gl_Position = u_Transform * a_Position;
v_TexCoord = a_TexCoord*u_texScale + u_texShift;
}

View File

@ -0,0 +1,24 @@
precision mediump float;
uniform vec4 u_Color;
varying vec4 v_Position;
void main(){
// Default alpha is 0
float alpha = 0.0;
// Radius is 0.5, since the diameter of our quad is 1
float radius = 0.5;
// Get the distance squared of from (0, 0)
float dist_sq = v_Position.x*v_Position.x + v_Position.y*v_Position.y;
if(dist_sq < radius*radius){
// Multiply by 4, since distance squared is at most 0.25
alpha = 4.0*dist_sq;
}
// Use the alpha value in our color
gl_FragColor = vec4(u_Color.rgb, alpha);
}

View File

@ -0,0 +1,11 @@
attribute vec4 a_Position;
uniform mat4 u_Transform;
varying vec4 v_Position;
void main(){
gl_Position = u_Transform * a_Position;
v_Position = a_Position;
}

View File

@ -0,0 +1,145 @@
{
"name": "player_spaceship",
"spriteSheetImage": "player_spaceship.png",
"spriteWidth": 256,
"spriteHeight": 256,
"leftBuffer": 0,
"rightBuffer": 0,
"topBuffer": 0,
"bottomBuffer": 0,
"columns": 5,
"rows": 5,
"animations": [
{
"name": "idle",
"repeat": true,
"frames": [
{
"index": 0,
"duration": 10
},
{
"index": 1,
"duration": 10
},
{
"index": 2,
"duration": 10
}
]
},
{
"name": "boost",
"repeat": true,
"frames": [
{
"index": 3,
"duration": 10
},
{
"index": 4,
"duration": 10
},
{
"index": 5,
"duration": 10
}
]
},
{
"name": "shield",
"repeat": false,
"frames": [
{
"index": 6,
"duration": 10
},
{
"index": 7,
"duration": 10
},
{
"index": 8,
"duration": 10
},
{
"index": 9,
"duration": 10
},
{
"index": 10,
"duration": 10
},
{
"index": 11,
"duration": 10
},
{
"index": 12,
"duration": 10
}
]
},
{
"name": "explode",
"repeat": false,
"frames": [
{
"index": 13,
"duration": 10
},
{
"index": 14,
"duration": 10
},
{
"index": 15,
"duration": 10
},
{
"index": 16,
"duration": 10
},
{
"index": 17,
"duration": 10
},
{
"index": 18,
"duration": 10
},
{
"index": 19,
"duration": 10
},
{
"index": 20,
"duration": 10
},
{
"index": 21,
"duration": 10
},
{
"index": 22,
"duration": 10
},
{
"index": 23,
"duration": 10
}
]
},
{
"name": "explode",
"repeat": false,
"onEnd": "dead",
"frames": [
{
"index": 24,
"duration": 1
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

48
src/WebGLScene.ts Normal file
View File

@ -0,0 +1,48 @@
import Vec2 from "./Wolfie2D/DataTypes/Vec2";
import { GraphicType } from "./Wolfie2D/Nodes/Graphics/GraphicTypes";
import Point from "./Wolfie2D/Nodes/Graphics/Point";
import Rect from "./Wolfie2D/Nodes/Graphics/Rect";
import AnimatedSprite from "./Wolfie2D/Nodes/Sprites/AnimatedSprite";
import Sprite from "./Wolfie2D/Nodes/Sprites/Sprite";
import Scene from "./Wolfie2D/Scene/Scene";
import Color from "./Wolfie2D/Utils/Color";
export default class WebGLScene extends Scene {
private point: Point;
private rect: Rect;
private player: AnimatedSprite;
private t: number = 0;
loadScene() {
this.load.spritesheet("player", "hw1_assets/spritesheets/player_spaceship.json");
}
startScene() {
this.addLayer("primary");
this.point = this.add.graphic(GraphicType.POINT, "primary", {position: new Vec2(100, 100), size: new Vec2(10, 10)})
this.point.color = Color.CYAN;
console.log(this.point.color.toStringRGBA());
this.rect = <Rect>this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(300, 100), size: new Vec2(100, 50)});
this.rect.color = Color.ORANGE;
this.player = this.add.animatedSprite("player", "primary");
this.player.position.set(800, 500);
this.player.scale.set(0.5, 0.5);
this.player.animation.play("idle");
}
updateScene(deltaT: number) {
this.t += deltaT;
let s = Math.sin(this.t);
let c = Math.cos(this.t);
this.point.position.x = 100 + 100*c;
this.point.position.y = 100 + 100*s;
this.rect.rotation = this.t;
}
}

View File

@ -0,0 +1,167 @@
import Vec2 from "./Vec2";
/** A 4x4 matrix0 */
export default class Mat4x4 {
private mat: Float32Array;
constructor(){
this.mat = new Float32Array([
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0
]);
}
// Static members
static get IDENTITY(): Mat4x4 {
return new Mat4x4().identity();
}
static get ZERO(): Mat4x4 {
return new Mat4x4().zero();
}
// Accessors
set _00(x: number) {
this.mat[0] = x;
}
set(col: number, row: number, value: number): Mat4x4 {
if(col < 0 || col > 3 || row < 0 || row > 3){
throw `Error - index (${col}, ${row}) is out of bounds for Mat4x4`
}
this.mat[row*4 + col] = value;
return this;
}
get(col: number, row: number): number {
return this.mat[row*4 + col];
}
setAll(...items: Array<number>): Mat4x4 {
this.mat.set(items);
return this;
}
identity(): Mat4x4 {
return this.setAll(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
)
}
zero(): Mat4x4 {
return this.setAll(
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0
);
}
/**
* Makes this Mat4x4 a rotation matrix of the specified number of radians ccw
* @param zRadians The number of radians to rotate
* @returns this Mat4x4
*/
rotate(zRadians: number): Mat4x4 {
return this.setAll(
Math.cos(zRadians), -Math.sin(zRadians), 0, 0,
Math.sin(zRadians), Math.cos(zRadians), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
}
/**
* Turns this Mat4x4 into a translation matrix of the specified translation
* @param translation The translation in x and y
* @returns this Mat4x4
*/
translate(translation: Vec2 | Float32Array): Mat4x4 {
// If translation is a vec, get its array
if(translation instanceof Vec2){
translation = translation.toArray();
}
return this.setAll(
1, 0, 0, translation[0],
0, 1, 0, translation[1],
0, 0, 1, 0,
0, 0, 0, 1
);
}
scale(scale: Vec2 | Float32Array | number): Mat4x4 {
// Make sure scale is a float32Array
if(scale instanceof Vec2){
scale = scale.toArray();
} else if(!(scale instanceof Float32Array)){
scale = new Float32Array([scale, scale]);
}
return this.setAll(
scale[0], 0, 0, 0,
0, scale[1], 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
}
/**
* Returns a new Mat4x4 that represents the right side multiplication THIS x OTHER
* @param other The other Mat4x4 to multiply by
* @returns a new Mat4x4 containing the product of these two Mat4x4s
*/
mult(other: Mat4x4, out?: Mat4x4): Mat4x4 {
let temp = new Float32Array(16);
for(let i = 0; i < 4; i++){
for(let j = 0; j < 4; j++){
let value = 0;
for(let k = 0; k < 4; k++){
value += this.get(k, i) * other.get(j, k);
}
temp[j*4 + i] = value;
}
}
if(out !== undefined){
return out.setAll(...temp);
} else {
return new Mat4x4().setAll(...temp);
}
}
/**
* Multiplies all given matricies in order. e.g. MULT(A, B, C) -> A*B*C
* @param mats A list of Mat4x4s to multiply in order
* @returns A new Mat4x4 holding the result of the operation
*/
static MULT(...mats: Array<Mat4x4>): Mat4x4 {
// Create a new array
let temp = Mat4x4.IDENTITY;
// Multiply by every array in order, in place
for(let i = 0; i < mats.length; i++){
temp.mult(mats[i], temp);
}
return temp;
}
toArray(): Float32Array {
return this.mat;
}
toString(): string {
return `|${this.mat[0].toFixed(2)}, ${this.mat[1].toFixed(2)}, ${this.mat[2].toFixed(2)}, ${this.mat[3].toFixed(2)}|\n` +
`|${this.mat[4].toFixed(2)}, ${this.mat[5].toFixed(2)}, ${this.mat[6].toFixed(2)}, ${this.mat[7].toFixed(2)}|\n` +
`|${this.mat[8].toFixed(2)}, ${this.mat[9].toFixed(2)}, ${this.mat[10].toFixed(2)}, ${this.mat[11].toFixed(2)}|\n` +
`|${this.mat[12].toFixed(2)}, ${this.mat[13].toFixed(2)}, ${this.mat[14].toFixed(2)}, ${this.mat[15].toFixed(2)}|`;
}
}

View File

@ -0,0 +1,5 @@
export default class WebGLGameTexture {
webGLTextureId: number;
webGLTexture: WebGLTexture;
imageKey: string;
}

View File

@ -0,0 +1,29 @@
/** A container for info about a webGL shader program */
export default class WebGLProgramType {
/** A webGL program */
program: WebGLProgram;
/** A vertex shader */
vertexShader: WebGLShader;
/** A fragment shader */
fragmentShader: WebGLShader;
/**
* Deletes this shader program
*/
delete(gl: WebGLRenderingContext): void {
// Clean up all aspects of this program
if(this.program){
gl.deleteProgram(this.program);
}
if(this.vertexShader){
gl.deleteShader(this.vertexShader);
}
if(this.fragmentShader){
gl.deleteShader(this.fragmentShader);
}
}
}

View File

@ -383,6 +383,10 @@ export default class Vec2 {
this.onChange = f; this.onChange = f;
} }
toArray(): Float32Array {
return this.vec;
}
/** /**
* Performs linear interpolation between two vectors * Performs linear interpolation between two vectors
* @param a The first vector * @param a The first vector

View File

@ -16,6 +16,8 @@ import GameLoop from "./GameLoop";
import FixedUpdateGameLoop from "./FixedUpdateGameLoop"; import FixedUpdateGameLoop from "./FixedUpdateGameLoop";
import EnvironmentInitializer from "./EnvironmentInitializer"; import EnvironmentInitializer from "./EnvironmentInitializer";
import Vec2 from "../DataTypes/Vec2"; import Vec2 from "../DataTypes/Vec2";
import Registry from "../Registry/Registry";
import WebGLRenderer from "../Rendering/WebGLRenderer";
/** /**
* The main loop of the game engine. * The main loop of the game engine.
@ -36,7 +38,7 @@ export default class Game {
readonly WIDTH: number; readonly WIDTH: number;
readonly HEIGHT: number; readonly HEIGHT: number;
private viewport: Viewport; private viewport: Viewport;
private ctx: CanvasRenderingContext2D; private ctx: CanvasRenderingContext2D | WebGLRenderingContext;
private clearColor: Color; private clearColor: Color;
// All of the necessary subsystems that need to run here // All of the necessary subsystems that need to run here
@ -73,8 +75,12 @@ export default class Game {
this.WIDTH = this.gameOptions.canvasSize.x; this.WIDTH = this.gameOptions.canvasSize.x;
this.HEIGHT = this.gameOptions.canvasSize.y; this.HEIGHT = this.gameOptions.canvasSize.y;
// For now, just hard code a canvas renderer. We can do this with options later // This step MUST happen before the resource manager does anything
if(this.gameOptions.useWebGL){
this.renderingManager = new WebGLRenderer();
} else {
this.renderingManager = new CanvasRenderer(); this.renderingManager = new CanvasRenderer();
}
this.initializeGameWindow(); this.initializeGameWindow();
this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT); this.ctx = this.renderingManager.initializeCanvas(this.GAME_CANVAS, this.WIDTH, this.HEIGHT);
this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b); this.clearColor = new Color(this.gameOptions.clearColor.r, this.gameOptions.clearColor.g, this.gameOptions.clearColor.b);
@ -131,8 +137,14 @@ export default class Game {
// Set the render function of the loop // Set the render function of the loop
this.loop.doRender = () => this.render(); this.loop.doRender = () => this.render();
// Start the loop // Preload registry items
Registry.preload();
// Load the items with the resource manager
this.resourceManager.loadResourcesFromQueue(() => {
// When we're dont loading, start the loop
this.loop.start(); this.loop.start();
});
} }
/** /**
@ -164,12 +176,17 @@ export default class Game {
*/ */
render(): void { render(): void {
// Clear the canvases // Clear the canvases
this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT);
Debug.clearCanvas(); Debug.clearCanvas();
// Game Canvas if(this.gameOptions.useWebGL){
this.ctx.fillStyle = this.clearColor.toString(); (<WebGLRenderingContext>this.ctx).clearColor(this.clearColor.r, this.clearColor.g, this.clearColor.b, this.clearColor.a);
this.ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT); (<WebGLRenderingContext>this.ctx).clear((<WebGLRenderingContext>this.ctx).COLOR_BUFFER_BIT | (<WebGLRenderingContext>this.ctx).DEPTH_BUFFER_BIT);
} else {
(<CanvasRenderingContext2D>this.ctx).clearRect(0, 0, this.WIDTH, this.HEIGHT);
(<CanvasRenderingContext2D>this.ctx).fillStyle = this.clearColor.toString();
(<CanvasRenderingContext2D>this.ctx).fillRect(0, 0, this.WIDTH, this.HEIGHT);
}
this.sceneManager.render(); this.sceneManager.render();
// Debug render // Debug render

View File

@ -20,6 +20,9 @@ export default class GameOptions {
/* Whether or not the stats rendering should occur */ /* Whether or not the stats rendering should occur */
showStats: boolean; showStats: boolean;
/* Whether or not to use webGL */
useWebGL: boolean;
/** /**
* Parses the data in the raw options object * Parses the data in the raw options object
* @param options The game options as a Record * @param options The game options as a Record
@ -34,6 +37,7 @@ export default class GameOptions {
gOpt.inputs = options.inputs ? options.inputs : []; gOpt.inputs = options.inputs ? options.inputs : [];
gOpt.showDebug = !!options.showDebug; gOpt.showDebug = !!options.showDebug;
gOpt.showStats = !!options.showStats; gOpt.showStats = !!options.showStats;
gOpt.useWebGL = !!options.useWebGL;
return gOpt; return gOpt;
} }

View File

@ -12,6 +12,8 @@ export default abstract class CanvasNode extends GameNode implements Region {
private _size: Vec2; private _size: Vec2;
private _scale: Vec2; private _scale: Vec2;
private _boundary: AABB; private _boundary: AABB;
private _hasCustomShader: boolean;
private _customShaderKey: string;
/** A flag for whether or not the CanvasNode is visible */ /** A flag for whether or not the CanvasNode is visible */
visible: boolean = true; visible: boolean = true;
@ -24,6 +26,8 @@ export default abstract class CanvasNode extends GameNode implements Region {
this._scale.setOnChange(() => this.scaleChanged()); this._scale.setOnChange(() => this.scaleChanged());
this._boundary = new AABB(); this._boundary = new AABB();
this.updateBoundary(); this.updateBoundary();
this._hasCustomShader = false;
} }
get size(): Vec2 { get size(): Vec2 {
@ -56,6 +60,14 @@ export default abstract class CanvasNode extends GameNode implements Region {
this.scale.y = value; this.scale.y = value;
} }
get hasCustomShader(): boolean {
return this._hasCustomShader;
}
get customShaderKey(): string {
return this._customShaderKey;
}
// @override // @override
protected positionChanged(): void { protected positionChanged(): void {
super.positionChanged(); super.positionChanged();
@ -89,6 +101,15 @@ export default abstract class CanvasNode extends GameNode implements Region {
return this.boundary.halfSize.clone().scaled(zoom, zoom); return this.boundary.halfSize.clone().scaled(zoom, zoom);
} }
/**
* Adds a custom shader to this CanvasNode
* @param key The registry key of the ShaderType
*/
useCustomShader(key: string): void {
this._hasCustomShader = true;
this._customShaderKey = key;
}
/** /**
* Returns true if the point (x, y) is inside of this canvas object * Returns true if the point (x, y) is inside of this canvas object
* @param x The x position of the point * @param x The x position of the point

View File

@ -176,6 +176,7 @@ export default abstract class GameNode implements Positioned, Unique, Updateable
// Set the collision shape if provided, or simply use the the region if there is one. // Set the collision shape if provided, or simply use the the region if there is one.
if(collisionShape){ if(collisionShape){
this.collisionShape = collisionShape; this.collisionShape = collisionShape;
this.collisionShape.center = this.position;
} else if (isRegion(this)) { } else if (isRegion(this)) {
// If the gamenode has a region and no other is specified, use that // If the gamenode has a region and no other is specified, use that
this.collisionShape = (<any>this).boundary.clone(); this.collisionShape = (<any>this).boundary.clone();

View File

@ -8,9 +8,17 @@ export default class AnimatedSprite extends Sprite {
/** The number of columns in this sprite sheet */ /** The number of columns in this sprite sheet */
protected numCols: number; protected numCols: number;
get cols(): number {
return this.numCols;
}
/** The number of rows in this sprite sheet */ /** The number of rows in this sprite sheet */
protected numRows: number; protected numRows: number;
get rows(): number {
return this.numRows;
}
/** The animationManager for this sprite */ /** The animationManager for this sprite */
animation: AnimationManager; animation: AnimationManager;

View File

@ -0,0 +1,98 @@
import Map from "../../DataTypes/Map";
import ShaderType from "../../Rendering/WebGLRendering/ShaderType";
import PointShaderType from "../../Rendering/WebGLRendering/ShaderTypes/PointShaderType";
import RectShaderType from "../../Rendering/WebGLRendering/ShaderTypes/RectShaderType";
import SpriteShaderType from "../../Rendering/WebGLRendering/ShaderTypes/SpriteShaderType";
import ResourceManager from "../../ResourceManager/ResourceManager";
/**
* A registry that handles shaders
*/
export default class ShaderRegistry extends Map<ShaderType> {
// Shader names
public static POINT_SHADER = "point";
public static RECT_SHADER = "rect";
public static SPRITE_SHADER = "sprite";
private registryItems: Array<ShaderRegistryItem> = new Array();
/**
* Preloads all built-in shaders
*/
public preload(){
console.log("Preloading");
// Get the resourceManager and queue all built-in shaders for preloading
const rm = ResourceManager.getInstance();
// Queue a load for the point shader
this.registerAndPreloadItem(ShaderRegistry.POINT_SHADER, PointShaderType, "builtin/shaders/point.vshader", "builtin/shaders/point.fshader");
// Queue a load for the rect shader
this.registerAndPreloadItem(ShaderRegistry.RECT_SHADER, RectShaderType, "builtin/shaders/rect.vshader", "builtin/shaders/rect.fshader");
// Queue a load for the sprite shader
this.registerAndPreloadItem(ShaderRegistry.SPRITE_SHADER, SpriteShaderType, "builtin/shaders/sprite.vshader", "builtin/shaders/sprite.fshader");
// Queue a load for any preloaded items
for(let item of this.registryItems){
const shader = new item.constr(item.key);
shader.initBufferObject();
this.add(item.key, shader);
console.log("Added", item.key);
// Load if desired
if(item.preload !== undefined){
console.log("Preloading", item.key);
rm.shader(item.key, item.preload.vshaderLocation, item.preload.fshaderLocation);
}
}
}
/**
* Registers a shader in the registry and loads it before the game begins
* @param key The key you wish to assign to the shader
* @param constr The constructor of the ShaderType
* @param vshaderLocation The location of the vertex shader
* @param fshaderLocation the location of the fragment shader
*/
public registerAndPreloadItem(key: string, constr: new (programKey: string) => ShaderType, vshaderLocation: string, fshaderLocation: string): void {
let shaderPreload = new ShaderPreload();
shaderPreload.vshaderLocation = vshaderLocation;
shaderPreload.fshaderLocation = fshaderLocation;
let registryItem = new ShaderRegistryItem();
registryItem.key = key;
registryItem.constr = constr;
registryItem.preload = shaderPreload;
this.registryItems.push(registryItem);
}
/**
* Registers a shader in the registry. NOTE: If you use this, you MUST load the shader before use.
* If you wish to preload the shader, use registerAndPreloadItem()
* @param key The key you wish to assign to the shader
* @param constr The constructor of the ShaderType
*/
public registerItem(key: string, constr: new (programKey: string) => ShaderType): void {
let registryItem = new ShaderRegistryItem();
registryItem.key = key;
registryItem.constr = constr;
this.registryItems.push(registryItem);
}
}
class ShaderRegistryItem {
key: string;
constr: new (programKey: string) => ShaderType;
preload: ShaderPreload;
}
class ShaderPreload {
vshaderLocation: string;
fshaderLocation: string;
}

View File

@ -0,0 +1,16 @@
import ShaderRegistry from "./Registries/ShaderRegistry";
/**
* The Registry is the system's way of converting classes and types into string
* representations for use elsewhere in the application.
* It allows classes to be accessed without explicitly using constructors in code,
* and for resources to be loaded at Game creation time.
*/
export default class Registry {
public static shaders = new ShaderRegistry();
static preload(){
this.shaders.preload();
}
}

View File

@ -85,6 +85,15 @@ export default class AnimationManager {
} }
} }
/**
* Determines whether the specified animation is currently playing
* @param key The key of the animation to check
* @returns true if the specified animation is playing, false otherwise
*/
isPlaying(key: string): boolean {
return this.currentAnimation === key && this.animationState === AnimationState.PLAYING;
}
/** /**
* Retrieves the current animation index and advances the animation frame * Retrieves the current animation index and advances the animation frame
* @returns The index of the animation frame * @returns The index of the animation frame
@ -147,7 +156,7 @@ export default class AnimationManager {
* @param loop Whether or not to loop the animation. False by default * @param loop Whether or not to loop the animation. False by default
* @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false. * @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false.
*/ */
playIfNotAlready(animation: string, loop: boolean = false, onEnd?: string): void { playIfNotAlready(animation: string, loop?: boolean, onEnd?: string): void {
if(this.currentAnimation !== animation){ if(this.currentAnimation !== animation){
this.play(animation, loop, onEnd); this.play(animation, loop, onEnd);
} }
@ -159,18 +168,27 @@ export default class AnimationManager {
* @param loop Whether or not to loop the animation. False by default * @param loop Whether or not to loop the animation. False by default
* @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false. * @param onEnd The name of an event to send when this animation naturally stops playing. This only matters if loop is false.
*/ */
play(animation: string, loop: boolean = false, onEnd?: string): void { play(animation: string, loop?: boolean, onEnd?: string): void {
this.currentAnimation = animation; this.currentAnimation = animation;
this.currentFrame = 0; this.currentFrame = 0;
this.frameProgress = 0; this.frameProgress = 0;
this.loop = loop;
this.animationState = AnimationState.PLAYING; this.animationState = AnimationState.PLAYING;
// If loop arg was provided, use that
if(loop !== undefined){
this.loop = loop;
} else {
// Otherwise, use what the json file specified
this.loop = this.animations.get(animation).repeat;
}
if(onEnd !== undefined){ if(onEnd !== undefined){
this.onEndEvent = onEnd; this.onEndEvent = onEnd;
} else { } else {
this.onEndEvent = null; this.onEndEvent = null;
} }
// Reset pending animation
this.pendingAnimation = null; this.pendingAnimation = null;
} }

View File

@ -12,6 +12,7 @@ export enum AnimationState {
export class AnimationData { export class AnimationData {
name: string; name: string;
frames: Array<{index: number, duration: number}>; frames: Array<{index: number, duration: number}>;
repeat: boolean = false;
} }
export class TweenData { export class TweenData {

View File

@ -131,7 +131,7 @@ export default class CanvasRenderer extends RenderingManager {
} }
this.ctx.setTransform(xScale, 0, 0, yScale, (node.position.x - this.origin.x)*this.zoom, (node.position.y - this.origin.y)*this.zoom); this.ctx.setTransform(xScale, 0, 0, yScale, (node.position.x - this.origin.x)*this.zoom, (node.position.y - this.origin.y)*this.zoom);
this.ctx.rotate(node.rotation); this.ctx.rotate(-node.rotation);
let globalAlpha = this.ctx.globalAlpha; let globalAlpha = this.ctx.globalAlpha;
this.ctx.globalAlpha = node.alpha; this.ctx.globalAlpha = node.alpha;

View File

@ -0,0 +1,136 @@
import Graph from "../DataTypes/Graphs/Graph";
import Map from "../DataTypes/Map";
import Vec2 from "../DataTypes/Vec2";
import CanvasNode from "../Nodes/CanvasNode";
import Graphic from "../Nodes/Graphic";
import { GraphicType } from "../Nodes/Graphics/GraphicTypes";
import Point from "../Nodes/Graphics/Point";
import Rect from "../Nodes/Graphics/Rect";
import AnimatedSprite from "../Nodes/Sprites/AnimatedSprite";
import Sprite from "../Nodes/Sprites/Sprite";
import Tilemap from "../Nodes/Tilemap";
import UIElement from "../Nodes/UIElement";
import ShaderRegistry from "../Registry/Registries/ShaderRegistry";
import Registry from "../Registry/Registry";
import ResourceManager from "../ResourceManager/ResourceManager";
import UILayer from "../Scene/Layers/UILayer";
import RenderingUtils from "../Utils/RenderingUtils";
import RenderingManager from "./RenderingManager";
import ShaderType from "./WebGLRendering/ShaderType";
export default class WebGLRenderer extends RenderingManager {
protected origin: Vec2;
protected zoom: number;
protected worldSize: Vec2;
protected gl: WebGLRenderingContext;
initializeCanvas(canvas: HTMLCanvasElement, width: number, height: number): WebGLRenderingContext {
canvas.width = width;
canvas.height = height;
this.worldSize = Vec2.ZERO;
this.worldSize.x = width;
this.worldSize.y = height;
// Get the WebGL context
this.gl = canvas.getContext("webgl");
this.gl.viewport(0, 0, canvas.width, canvas.height);
this.gl.disable(this.gl.DEPTH_TEST);
this.gl.enable(this.gl.BLEND);
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA);
this.gl.enable(this.gl.CULL_FACE);
// Tell the resource manager we're using WebGL
ResourceManager.getInstance().useWebGL(true, this.gl);
return this.gl;
}
render(visibleSet: CanvasNode[], tilemaps: Tilemap[], uiLayers: Map<UILayer>): void {
for(let node of visibleSet){
this.renderNode(node);
}
}
protected renderNode(node: CanvasNode): void {
// Calculate the origin of the viewport according to this sprite
this.origin = this.scene.getViewTranslation(node);
// Get the zoom level of the scene
this.zoom = this.scene.getViewScale();
if(node.hasCustomShader){
// If the node has a custom shader, render using that
this.renderCustom(node);
} else if(node instanceof Graphic){
this.renderGraphic(node);
} else if(node instanceof Sprite){
if(node instanceof AnimatedSprite){
this.renderAnimatedSprite(node);
} else {
this.renderSprite(node);
}
}
}
protected renderSprite(sprite: Sprite): void {
let shader = Registry.shaders.get(ShaderRegistry.SPRITE_SHADER);
let options = shader.getOptions(sprite);
options.worldSize = this.worldSize;
options.origin = this.origin;
shader.render(this.gl, options);
}
protected renderAnimatedSprite(sprite: AnimatedSprite): void {
let shader = Registry.shaders.get(ShaderRegistry.SPRITE_SHADER);
let options = shader.getOptions(sprite);
options.worldSize = this.worldSize;
options.origin = this.origin;
Registry.shaders.get(ShaderRegistry.SPRITE_SHADER).render(this.gl, options);
}
protected renderGraphic(graphic: Graphic): void {
if(graphic instanceof Point){
let shader = Registry.shaders.get(ShaderRegistry.POINT_SHADER);
let options = shader.getOptions(graphic);
options.worldSize = this.worldSize;
options.origin = this.origin;
shader.render(this.gl, options);
} else if(graphic instanceof Rect) {
let shader = Registry.shaders.get(ShaderRegistry.RECT_SHADER);
let options = shader.getOptions(graphic);
options.worldSize = this.worldSize;
options.origin = this.origin;
shader.render(this.gl, options);
}
}
protected renderTilemap(tilemap: Tilemap): void {
throw new Error("Method not implemented.");
}
protected renderUIElement(uiElement: UIElement): void {
throw new Error("Method not implemented.");
}
protected renderCustom(node: CanvasNode): void {
let shader = Registry.shaders.get(node.customShaderKey);
let options = shader.getOptions(node);
options.worldSize = this.worldSize;
options.origin = this.origin;
shader.render(this.gl, options);
}
}

View File

@ -0,0 +1,44 @@
import Map from "../../DataTypes/Map";
import CanvasNode from "../../Nodes/CanvasNode";
import ResourceManager from "../../ResourceManager/ResourceManager";
/**
* A wrapper class for WebGL shaders.
* This class is a singleton, and there is only one for each shader type.
* All objects that use this shader type will refer to and modify this same type.
*/
export default abstract class ShaderType {
/** The name of this shader */
protected name: string;
/** The key to the WebGLProgram in the ResourceManager */
protected programKey: string;
/** A reference to the resource manager */
protected resourceManager: ResourceManager;
constructor(programKey: string){
this.programKey = programKey;
this.resourceManager = ResourceManager.getInstance();
}
/**
* Initializes any buffer objects associated with this shader type.
* @param gl The WebGL rendering context
*/
abstract initBufferObject(): void;
/**
* Loads any uniforms
* @param gl The WebGL rendering context
* @param options Information about the object we're currently rendering
*/
abstract render(gl: WebGLRenderingContext, options: Record<string, any>): void;
/**
* Extracts the options from the CanvasNode and gives them to the render function
* @param node The node to get options from
* @returns An object containing the options that should be passed to the render function
*/
getOptions(node: CanvasNode): Record<string, any> {return {};}
}

View File

@ -0,0 +1,61 @@
import Debug from "../../../Debug/Debug";
import Point from "../../../Nodes/Graphics/Point";
import ResourceManager from "../../../ResourceManager/ResourceManager";
import RenderingUtils from "../../../Utils/RenderingUtils";
import ShaderType from "../ShaderType";
export default class PointShaderType extends ShaderType {
protected bufferObjectKey: string;
constructor(programKey: string){
super(programKey);
}
initBufferObject(): void {
this.bufferObjectKey = "point";
this.resourceManager.createBuffer(this.bufferObjectKey);
}
render(gl: WebGLRenderingContext, options: Record<string, any>): void {
let position = RenderingUtils.toWebGLCoords(options.position, options.origin, options.worldSize);
let color = RenderingUtils.toWebGLColor(options.color);
const program = this.resourceManager.getShaderProgram(this.programKey);
const buffer = this.resourceManager.getBuffer(this.bufferObjectKey);
gl.useProgram(program);
const vertexData = position;
const FSIZE = vertexData.BYTES_PER_ELEMENT;
// Bind the buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
// Attributes
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE);
gl.enableVertexAttribArray(a_Position);
// Uniforms
const u_Color = gl.getUniformLocation(program, "u_Color");
gl.uniform4fv(u_Color, color);
const u_PointSize = gl.getUniformLocation(program, "u_PointSize");
gl.uniform1f(u_PointSize, options.pointSize);
gl.drawArrays(gl.POINTS, 0, 1);
}
getOptions(point: Point): Record<string, any> {
let options: Record<string, any> = {
position: point.position,
color: point.color,
pointSize: point.size,
}
return options;
}
}

View File

@ -0,0 +1,25 @@
import Mat4x4 from "../../../DataTypes/Mat4x4";
import ShaderType from "../ShaderType";
/** Represents any WebGL objects that have a quad mesh (i.e. a rectangular game object composed of only two triangles) */
export default abstract class QuadShaderType extends ShaderType {
/** The key to the buffer object for this shader */
protected bufferObjectKey: string;
/** The scale matric */
protected scale: Mat4x4;
/** The rotation matrix */
protected rotation: Mat4x4;
/** The translation matrix */
protected translation: Mat4x4;
constructor(programKey: string){
super(programKey);
this.scale = Mat4x4.IDENTITY;
this.rotation = Mat4x4.IDENTITY;
this.translation = Mat4x4.IDENTITY;
}
}

View File

@ -0,0 +1,133 @@
import Mat4x4 from "../../../DataTypes/Mat4x4";
import Vec2 from "../../../DataTypes/Vec2";
import Rect from "../../../Nodes/Graphics/Rect";
import ResourceManager from "../../../ResourceManager/ResourceManager";
import QuadShaderType from "./QuadShaderType";
export default class RectShaderType extends QuadShaderType {
constructor(programKey: string){
super(programKey);
this.resourceManager = ResourceManager.getInstance();
}
initBufferObject(): void {
this.bufferObjectKey = "rect";
this.resourceManager.createBuffer(this.bufferObjectKey);
}
render(gl: WebGLRenderingContext, options: Record<string, any>): void {
const color = options.color.toWebGL();
const program = this.resourceManager.getShaderProgram(this.programKey);
const buffer = this.resourceManager.getBuffer(this.bufferObjectKey);
gl.useProgram(program);
const vertexData = this.getVertices(options.size.x, options.size.y);
const FSIZE = vertexData.BYTES_PER_ELEMENT;
// Bind the buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
// Attributes
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE);
gl.enableVertexAttribArray(a_Position);
// Uniforms
const u_Color = gl.getUniformLocation(program, "u_Color");
gl.uniform4fv(u_Color, color);
// Get transformation matrix
// We want a square for our rendering space, so get the maximum dimension of our quad
let maxDimension = Math.max(options.size.x, options.size.y);
// The size of the rendering space will be a square with this maximum dimension
let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y);
// Center our translations around (0, 0)
const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension;
const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension;
// Create our transformation matrix
this.translation.translate(new Float32Array([translateX, translateY]));
this.scale.scale(size);
this.rotation.rotate(options.rotation);
let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation);
// Pass the translation matrix to our shader
const u_Transform = gl.getUniformLocation(program, "u_Transform");
gl.uniformMatrix4fv(u_Transform, false, transformation.toArray());
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
/*
So as it turns out, WebGL has an issue with non-square quads.
It doesn't like when you don't have a 1-1 scale, and rotations are entirely messed up if this is not the case.
To solve this, I used the scale of the LARGEST dimension of the quad to make a square, then adjusted the vertex coordinates inside of that.
A diagram of the solution follows.
There is a bounding square for the quad with dimensions hxh (in this case, since height is the largest dimension).
The offset in the vertical direction is therefore 0.5, as it is normally.
However, the offset in the horizontal direction is not so straightforward, but isn't conceptually hard.
All we really have to do is a range change from [0, height/2] to [0, 0.5], where our value is t = width/2, and 0 <= t <= height/2.
So now we have our rect, in a space scaled with respect to the largest dimension.
Rotations work as you would expect, even for long rectangles.
0.5
__ __ __ __ __ __ __
| |88888888888| |
| |88888888888| |
| |88888888888| |
-0.5|_ _|88888888888|_ _|0.5
| |88888888888| |
| |88888888888| |
| |88888888888| |
|___|88888888888|___|
-0.5
The getVertices function below does as described, and converts the range
*/
/**
* The rendering space always has to be a square, so make sure its square w.r.t to the largest dimension
* @param w The width of the quad in pixels
* @param h The height of the quad in pixels
* @returns An array of the vertices of the quad
*/
getVertices(w: number, h: number): Float32Array {
let x, y;
if(h > w){
y = 0.5;
x = w/(2*h);
} else {
x = 0.5;
y = h/(2*w);
}
return new Float32Array([
-x, y,
-x, -y,
x, y,
x, -y
]);
}
getOptions(rect: Rect): Record<string, any> {
let options: Record<string, any> = {
position: rect.position,
color: rect.color,
size: rect.size,
rotation: rect.rotation
}
return options;
}
}

View File

@ -0,0 +1,153 @@
import Mat4x4 from "../../../DataTypes/Mat4x4";
import Vec2 from "../../../DataTypes/Vec2";
import Debug from "../../../Debug/Debug";
import AnimatedSprite from "../../../Nodes/Sprites/AnimatedSprite";
import Sprite from "../../../Nodes/Sprites/Sprite";
import ResourceManager from "../../../ResourceManager/ResourceManager";
import QuadShaderType from "./QuadShaderType";
/** A shader for sprites and animated sprites */
export default class SpriteShaderType extends QuadShaderType {
constructor(programKey: string){
super(programKey);
this.resourceManager = ResourceManager.getInstance();
}
initBufferObject(): void {
this.bufferObjectKey = "sprite";
this.resourceManager.createBuffer(this.bufferObjectKey);
}
render(gl: WebGLRenderingContext, options: Record<string, any>): void {
const program = this.resourceManager.getShaderProgram(this.programKey);
const buffer = this.resourceManager.getBuffer(this.bufferObjectKey);
const texture = this.resourceManager.getTexture(options.imageKey);
const image = this.resourceManager.getImage(options.imageKey);
gl.useProgram(program);
// Enable texture0
gl.activeTexture(gl.TEXTURE0);
// Bind our texture to texture 0
gl.bindTexture(gl.TEXTURE_2D, texture);
// Set the texture parameters
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
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);
// Set the texture image
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
const vertexData = this.getVertices(options.size.x, options.size.y, options.scale);
const FSIZE = vertexData.BYTES_PER_ELEMENT;
// Bind the buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
// Attributes
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 4 * FSIZE, 0 * FSIZE);
gl.enableVertexAttribArray(a_Position);
const a_TexCoord = gl.getAttribLocation(program, "a_TexCoord");
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, 4 * FSIZE, 2*FSIZE);
gl.enableVertexAttribArray(a_TexCoord);
// Uniforms
// Get transformation matrix
// We want a square for our rendering space, so get the maximum dimension of our quad
let maxDimension = Math.max(options.size.x, options.size.y);
// The size of the rendering space will be a square with this maximum dimension
let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y);
// Center our translations around (0, 0)
const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension;
const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension;
// Create our transformation matrix
this.translation.translate(new Float32Array([translateX, translateY]));
this.scale.scale(size);
this.rotation.rotate(options.rotation);
let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation);
// Pass the translation matrix to our shader
const u_Transform = gl.getUniformLocation(program, "u_Transform");
gl.uniformMatrix4fv(u_Transform, false, transformation.toArray());
// Set texture unit 0 to the sampler
const u_Sampler = gl.getUniformLocation(program, "u_Sampler");
gl.uniform1i(u_Sampler, 0);
// Pass in texShift
const u_texShift = gl.getUniformLocation(program, "u_texShift");
gl.uniform2fv(u_texShift, options.texShift);
// Pass in texScale
const u_texScale = gl.getUniformLocation(program, "u_texScale");
gl.uniform2fv(u_texScale, options.texScale);
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4 );
}
/**
* The rendering space always has to be a square, so make sure its square w.r.t to the largest dimension
* @param w The width of the quad in pixels
* @param h The height of the quad in pixels
* @returns An array of the vertices of the quad
*/
getVertices(w: number, h: number, scale: Float32Array): Float32Array {
let x, y;
if(h > w){
y = 0.5;
x = w/(2*h);
} else {
x = 0.5;
y = h/(2*w);
}
// Scale the rendering space if needed
x *= scale[0];
y *= scale[1];
return new Float32Array([
-x, y, 0.0, 0.0,
-x, -y, 0.0, 1.0,
x, y, 1.0, 0.0,
x, -y, 1.0, 1.0
]);
}
getOptions(sprite: Sprite): Record<string, any> {
let texShift;
let texScale;
if(sprite instanceof AnimatedSprite){
let animationIndex = sprite.animation.getIndexAndAdvanceAnimation();
let offset = sprite.getAnimationOffset(animationIndex);
texShift = new Float32Array([offset.x / (sprite.cols * sprite.size.x), offset.y / (sprite.rows * sprite.size.y)]);
texScale = new Float32Array([1/(sprite.cols), 1/(sprite.rows)]);
} else {
texShift = new Float32Array([0, 0]);
texScale = new Float32Array([1, 1]);
}
let options: Record<string, any> = {
position: sprite.position,
rotation: sprite.rotation,
size: sprite.size,
scale: sprite.scale.toArray(),
imageKey: sprite.imageId,
texShift,
texScale
}
return options;
}
}

View File

@ -4,6 +4,8 @@ import { TiledTilemapData } from "../DataTypes/Tilesets/TiledData";
import StringUtils from "../Utils/StringUtils"; import StringUtils from "../Utils/StringUtils";
import AudioManager from "../Sound/AudioManager"; import AudioManager from "../Sound/AudioManager";
import Spritesheet from "../DataTypes/Spritesheet"; import Spritesheet from "../DataTypes/Spritesheet";
import WebGLProgramType from "../DataTypes/Rendering/WebGLProgramType";
import PhysicsManager from "../Physics/PhysicsManager";
/** /**
* The resource manager for the game engine. * The resource manager for the game engine.
@ -68,6 +70,21 @@ export default class ResourceManager {
/** The total number of "types" of things that need to be loaded (i.e. images and tilemaps) */ /** The total number of "types" of things that need to be loaded (i.e. images and tilemaps) */
private loadonly_typesToLoad: number; private loadonly_typesToLoad: number;
/* ########## INFORMATION SPECIAL TO WEBGL ########## */
private gl_WebGLActive: boolean;
private loadonly_gl_ShaderProgramsLoaded: number;
private loadonly_gl_ShaderProgramsToLoad: number;
private loadonly_gl_ShaderLoadingQueue: Queue<KeyPath_Shader>;
private gl_DefaultShaderPrograms: Map<WebGLProgramType>;
private gl_ShaderPrograms: Map<WebGLProgramType>;
private gl_Textures: Map<WebGLTexture>;
private gl_Buffers: Map<WebGLBuffer>;
private gl: WebGLRenderingContext;
private constructor(){ private constructor(){
this.loading = false; this.loading = false;
this.justLoaded = false; this.justLoaded = false;
@ -91,6 +108,16 @@ export default class ResourceManager {
this.loadonly_audioToLoad = 0; this.loadonly_audioToLoad = 0;
this.loadonly_audioLoadingQueue = new Queue(); this.loadonly_audioLoadingQueue = new Queue();
this.audioBuffers = new Map(); this.audioBuffers = new Map();
this.loadonly_gl_ShaderProgramsLoaded = 0;
this.loadonly_gl_ShaderProgramsToLoad = 0;
this.loadonly_gl_ShaderLoadingQueue = new Queue();
this.gl_DefaultShaderPrograms = new Map();
this.gl_ShaderPrograms = new Map();
this.gl_Textures = new Map();
this.gl_Buffers = new Map();
}; };
/** /**
@ -105,6 +132,19 @@ export default class ResourceManager {
return this.instance; return this.instance;
} }
/**
* Activates or deactivates the use of WebGL
* @param flag True if WebGL should be used, false otherwise
* @param gl The instance of the graphics context, if applicable
*/
public useWebGL(flag: boolean, gl: WebGLRenderingContext): void {
this.gl_WebGLActive = flag;
if(this.gl_WebGLActive){
this.gl = gl;
}
}
/** /**
* Loads an image from file * Loads an image from file
* @param key The key to associate the loaded image with * @param key The key to associate the loaded image with
@ -199,15 +239,26 @@ export default class ResourceManager {
console.log("Loaded Images"); console.log("Loaded Images");
this.loadAudioFromQueue(() => { this.loadAudioFromQueue(() => {
console.log("Loaded Audio"); console.log("Loaded Audio");
if(this.gl_WebGLActive){
this.gl_LoadShadersFromQueue(() => {
console.log("Loaded Shaders");
this.finishLoading(callback);
});
} else {
this.finishLoading(callback);
}
});
});
});
});
}
private finishLoading(callback: Function): void {
// Done loading // Done loading
this.loading = false; this.loading = false;
this.justLoaded = true; this.justLoaded = true;
callback(); callback();
});
});
});
});
} }
/** /**
@ -232,6 +283,12 @@ export default class ResourceManager {
this.loadonly_audioLoaded = 0; this.loadonly_audioLoaded = 0;
this.loadonly_audioToLoad = 0; this.loadonly_audioToLoad = 0;
this.audioBuffers.clear(); this.audioBuffers.clear();
// WebGL
// Delete all programs through webGL
this.gl_ShaderPrograms.forEach(key => this.gl_ShaderPrograms.get(key).delete(this.gl));
this.gl_ShaderPrograms.clear();
this.gl_Textures.clear();
} }
/** /**
@ -385,6 +442,9 @@ export default class ResourceManager {
// Add to loaded images // Add to loaded images
this.images.add(key, image); this.images.add(key, image);
// If WebGL is active, create a texture
this.createWebGLTexture(key);
// Finish image load // Finish image load
this.finishLoadingImage(callbackIfLast); this.finishLoadingImage(callbackIfLast);
} }
@ -464,6 +524,192 @@ export default class ResourceManager {
} }
} }
/* ########## WEBGL SPECIFIC FUNCTIONS ########## */
public getTexture(key: string): WebGLTexture {
return this.gl_Textures.get(key);
}
public getShaderProgram(key: string): WebGLProgram {
return this.gl_ShaderPrograms.get(key).program;
}
public getBuffer(key: string): WebGLBuffer {
return this.gl_Buffers.get(key);
}
private createWebGLTexture(key:string): void {
if(this.gl_WebGLActive){
const texture = this.gl.createTexture();
this.gl_Textures.add(key, texture);
}
}
public createBuffer(key: string): void {
if(this.gl_WebGLActive){
let buffer = this.gl.createBuffer();
this.gl_Buffers.add(key, buffer);
}
}
/**
* Enqueues loading of a new shader program
* @param key The key of the shader program
* @param vShaderFilepath
* @param fShaderFilepath
*/
public shader(key: string, vShaderFilepath: string, fShaderFilepath: string): void {
let splitPath = vShaderFilepath.split(".");
let end = splitPath[splitPath.length - 1];
if(end !== "vshader"){
throw `${vShaderFilepath} is not a valid vertex shader - must end in ".vshader`;
}
splitPath = fShaderFilepath.split(".");
end = splitPath[splitPath.length - 1];
if(end !== "fshader"){
throw `${fShaderFilepath} is not a valid vertex shader - must end in ".fshader`;
}
let paths = new KeyPath_Shader();
paths.key = key;
paths.vpath = vShaderFilepath;
paths.fpath = fShaderFilepath;
this.loadonly_gl_ShaderLoadingQueue.enqueue(paths);
}
private gl_LoadShadersFromQueue(onFinishLoading: Function): void {
this.loadonly_gl_ShaderProgramsToLoad = this.loadonly_gl_ShaderLoadingQueue.getSize();
this.loadonly_gl_ShaderProgramsLoaded = 0;
// If webGL isn'active or there are no items to load, we're finished
if(!this.gl_WebGLActive || this.loadonly_gl_ShaderProgramsToLoad === 0){
onFinishLoading();
}
while(this.loadonly_gl_ShaderLoadingQueue.hasItems()){
let shader = this.loadonly_gl_ShaderLoadingQueue.dequeue();
this.gl_LoadShader(shader.key, shader.vpath, shader.fpath, onFinishLoading);
}
}
private gl_LoadShader(key: string, vpath: string, fpath: string, callbackIfLast: Function): void {
this.loadTextFile(vpath, (vFileText: string) => {
const vShader = vFileText;
this.loadTextFile(fpath, (fFileText: string) => {
const fShader = fFileText
// Extract the program and shaders
const [shaderProgram, vertexShader, fragmentShader] = this.createShaderProgram(vShader, fShader);
// Create a wrapper type
const programWrapper = new WebGLProgramType();
programWrapper.program = shaderProgram;
programWrapper.vertexShader = vertexShader;
programWrapper.fragmentShader = fragmentShader;
// Add to our map
this.gl_ShaderPrograms.add(key, programWrapper);
// Finish loading
this.gl_FinishLoadingShader(callbackIfLast);
});
});
}
private gl_FinishLoadingShader(callback: Function): void {
this.loadonly_gl_ShaderProgramsLoaded += 1;
if(this.loadonly_gl_ShaderProgramsLoaded === this.loadonly_gl_ShaderProgramsToLoad){
// We're done loading shaders
callback();
}
}
private createShaderProgram(vShaderSource: string, fShaderSource: string){
const vertexShader = this.loadVertexShader(vShaderSource);
const fragmentShader = this.loadFragmentShader(fShaderSource);
if(vertexShader === null || fragmentShader === null){
// We had a problem intializing - error
return null;
}
// Create a shader program
const program = this.gl.createProgram();
if(!program) {
// Error creating
console.log("Failed to create program");
return null;
}
// Attach our vertex and fragment shader
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
// Link
this.gl.linkProgram(program);
if(!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)){
// Error linking
const error = this.gl.getProgramInfoLog(program);
console.log("Failed to link program: " + error);
// Clean up
this.gl.deleteProgram(program);
this.gl.deleteShader(vertexShader);
this.gl.deleteShader(fragmentShader);
return null;
}
// We successfully create a program
return [program, vertexShader, fragmentShader];
}
private loadVertexShader(shaderSource: string): WebGLShader{
// Create a new vertex shader
return this.loadShader(this.gl.VERTEX_SHADER, shaderSource);
}
private loadFragmentShader(shaderSource: string): WebGLShader{
// Create a new fragment shader
return this.loadShader(this.gl.FRAGMENT_SHADER, shaderSource);
}
private loadShader(type: number, shaderSource: string): WebGLShader{
const shader = this.gl.createShader(type);
// If we couldn't create the shader, error
if(shader === null){
console.log("Unable to create shader");
return null;
}
// Add the source to the shader and compile
this.gl.shaderSource(shader, shaderSource);
this.gl.compileShader(shader);
// Make sure there were no errors during this process
if(!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)){
// Not compiled - error
const error = this.gl.getShaderInfoLog(shader);
console.log("Failed to compile shader: " + error);
// Clean up
this.gl.deleteShader(shader);
return null;
}
// Sucess, so return the shader
return shader;
}
/* ########## GENERAL LOADING FUNCTIONS ########## */
private loadTextFile(textFilePath: string, callback: Function): void { private loadTextFile(textFilePath: string, callback: Function): void {
let xobj: XMLHttpRequest = new XMLHttpRequest(); let xobj: XMLHttpRequest = new XMLHttpRequest();
xobj.overrideMimeType("application/json"); xobj.overrideMimeType("application/json");
@ -476,6 +722,8 @@ export default class ResourceManager {
xobj.send(null); xobj.send(null);
} }
/* ########## LOADING BAR INFO ########## */
private getLoadPercent(): number { private getLoadPercent(): number {
return (this.loadonly_tilemapsLoaded/this.loadonly_tilemapsToLoad return (this.loadonly_tilemapsLoaded/this.loadonly_tilemapsToLoad
+ this.loadonly_spritesheetsLoaded/this.loadonly_spritesheetsToLoad + this.loadonly_spritesheetsLoaded/this.loadonly_spritesheetsToLoad
@ -499,6 +747,12 @@ export default class ResourceManager {
} }
class KeyPathPair { class KeyPathPair {
key: string key: string;
path: string path: string;
}
class KeyPath_Shader {
key: string;
vpath: string;
fpath: string;
} }

View File

@ -88,8 +88,10 @@ export default class SceneManager {
* Renders the current Scene * Renders the current Scene
*/ */
public render(): void { public render(): void {
if(this.currentScene.isRunning()){
this.currentScene.render(); this.currentScene.render();
} }
}
/** /**
* Updates the current Scene * Updates the current Scene

View File

@ -83,7 +83,7 @@ export default class SceneGraphArray extends SceneGraph {
let visibleSet = new Array<CanvasNode>(); let visibleSet = new Array<CanvasNode>();
for(let node of this.nodeList){ for(let node of this.nodeList){
if(!node.getLayer().isHidden() && this.viewport.includes(node)){ if(!node.getLayer().isHidden() && node.visible && this.viewport.includes(node)){
visibleSet.push(node); visibleSet.push(node);
} }
} }

View File

@ -138,6 +138,14 @@ export default class Color {
return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), this.a); return new Color(MathUtils.clamp(this.r - 40, 0, 255), MathUtils.clamp(this.g - 40, 0, 255), MathUtils.clamp(this.b - 40, 0, 255), this.a);
} }
/**
* Returns this color as an array
* @returns [r, g, b, a]
*/
toArray(): [number, number, number, number] {
return [this.r, this.g, this.b, this.a];
}
/** /**
* Returns the color as a string of the form #RRGGBB * Returns the color as a string of the form #RRGGBB
* @returns #RRGGBB * @returns #RRGGBB
@ -164,4 +172,17 @@ export default class Color {
} }
return "rgba(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ", " + this.a.toString() +")" return "rgba(" + this.r.toString() + ", " + this.g.toString() + ", " + this.b.toString() + ", " + this.a.toString() +")"
} }
/**
* Turns this color into a float32Array and changes color range to [0.0, 1.0]
* @returns a Float32Array containing the color
*/
toWebGL(): Float32Array {
return new Float32Array([
this.r/255,
this.g/255,
this.b/255,
this.a
]);
}
} }

View File

@ -57,6 +57,10 @@ export default class MathUtils {
} }
} }
static changeRange(x: number, min: number, max: number, newMin: number, newMax: number): number {
return this.lerp(newMin, newMax, this.invLerp(min, max, x));
}
/** /**
* Linear Interpolation * Linear Interpolation
* @param a The first value for the interpolation bound * @param a The first value for the interpolation bound

View File

@ -0,0 +1,28 @@
import Vec2 from "../DataTypes/Vec2";
import Color from "./Color";
import MathUtils from "./MathUtils";
export default class RenderingUtils {
static toWebGLCoords(point: Vec2, origin: Vec2, worldSize: Vec2): Float32Array {
return new Float32Array([
MathUtils.changeRange(point.x, origin.x, origin.x + worldSize.x, -1, 1),
MathUtils.changeRange(point.y, origin.y, origin.y + worldSize.y, 1, -1)
]);
}
static toWebGLScale(size: Vec2, worldSize: Vec2): Float32Array {
return new Float32Array([
2*size.x/worldSize.x,
2*size.y/worldSize.y,
]);
}
static toWebGLColor(color: Color): Float32Array {
return new Float32Array([
MathUtils.changeRange(color.r, 0, 255, 0, 1),
MathUtils.changeRange(color.g, 0, 255, 0, 1),
MathUtils.changeRange(color.b, 0, 255, 0, 1),
color.a
]);
}
}

View File

@ -0,0 +1,68 @@
import Map from "../Wolfie2D/DataTypes/Map";
import Mat4x4 from "../Wolfie2D/DataTypes/Mat4x4";
import Vec2 from "../Wolfie2D/DataTypes/Vec2";
import RectShaderType from "../Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType";
/**
* The gradient circle is technically rendered on a quad, and is similar to a rect, so we'll extend the RectShaderType
*/
export default class GradientCircleShaderType extends RectShaderType {
initBufferObject(): void {
this.bufferObjectKey = "gradient_circle";
this.resourceManager.createBuffer(this.bufferObjectKey);
}
render(gl: WebGLRenderingContext, options: Record<string, any>): void {
// Get our program and buffer object
const program = this.resourceManager.getShaderProgram(this.programKey);
const buffer = this.resourceManager.getBuffer(this.bufferObjectKey);
// Let WebGL know we're using our shader program
gl.useProgram(program);
// Get our vertex data
const vertexData = this.getVertices(options.size.x, options.size.y);
const FSIZE = vertexData.BYTES_PER_ELEMENT;
// Bind the buffer
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertexData, gl.STATIC_DRAW);
/* ##### ATTRIBUTES ##### */
// No texture, the only thing we care about is vertex position
const a_Position = gl.getAttribLocation(program, "a_Position");
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 2 * FSIZE, 0 * FSIZE);
gl.enableVertexAttribArray(a_Position);
/* ##### UNIFORMS ##### */
// Send the color to the gradient circle
const color = options.color.toWebGL();
const u_Color = gl.getUniformLocation(program, "u_Color");
gl.uniform4fv(u_Color, color);
// Get transformation matrix
// We have a square for our rendering space, so get the maximum dimension of our quad
let maxDimension = Math.max(options.size.x, options.size.y);
// The size of the rendering space will be a square with this maximum dimension
let size = new Vec2(maxDimension, maxDimension).scale(2/options.worldSize.x, 2/options.worldSize.y);
// Center our translations around (0, 0)
const translateX = (options.position.x - options.origin.x - options.worldSize.x/2)/maxDimension;
const translateY = -(options.position.y - options.origin.y - options.worldSize.y/2)/maxDimension;
// Create our transformation matrix
this.translation.translate(new Float32Array([translateX, translateY]));
this.scale.scale(size);
this.rotation.rotate(options.rotation);
let transformation = Mat4x4.MULT(this.translation, this.scale, this.rotation);
// Pass the translation matrix to our shader
const u_Transform = gl.getUniformLocation(program, "u_Transform");
gl.uniformMatrix4fv(u_Transform, false, transformation.toArray());
// Draw the quad
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
}

8
src/hw1/HW1_Enums.ts Normal file
View File

@ -0,0 +1,8 @@
export enum Homework1Event {
PLAYER_DAMAGE = "PLAYER_DAMAGE",
SPAWN_FLEET = "SPAWN_FLEET"
}
export enum Homework1Shaders {
GRADIENT_CIRCLE = "GRADIENT_CIRCLE"
}

107
src/hw1/HW1_Scene.ts Normal file
View File

@ -0,0 +1,107 @@
import AABB from "../Wolfie2D/DataTypes/Shapes/AABB";
import Vec2 from "../Wolfie2D/DataTypes/Vec2";
import Graphic from "../Wolfie2D/Nodes/Graphic";
import { GraphicType } from "../Wolfie2D/Nodes/Graphics/GraphicTypes";
import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite";
import Scene from "../Wolfie2D/Scene/Scene";
import { Homework1Event, Homework1Shaders } from "./HW1_Enums";
import SpaceshipPlayerController from "./SpaceshipPlayerController";
/**
* In Wolfie2D, custom scenes extend the original scene class.
* This gives us access to lifecycle methods to control our game.
*/
export default class Homework1_Scene extends Scene {
// Here we define member variables of our game, and object pools for adding in game objects
private player: AnimatedSprite;
// Create an object pool for our fleet
private MAX_FLEET_SIZE = 20;
private fleet: Array<AnimatedSprite> = new Array(this.MAX_FLEET_SIZE);
// Create an object pool for our fleet
private MAX_NUM_ASTEROIDS = 6;
private asteroids: Array<Graphic> = new Array(this.MAX_NUM_ASTEROIDS);
// Create an object pool for our fleet
private MAX_NUM_MINERALS = 20;
private minerals: Array<Graphic> = new Array(this.MAX_NUM_MINERALS);
/*
* loadScene() overrides the parent class method. It allows us to load in custom assets for
* use in our scene.
*/
loadScene(){
/* ##### DO NOT MODIFY ##### */
// Load in the player spaceship spritesheet
this.load.spritesheet("player", "hw1_assets/spritesheets/player_spaceship.json");
/* ##### YOUR CODE GOES BELOW THIS LINE ##### */
}
/*
* startScene() allows us to add in the assets we loaded in loadScene() as game objects.
* Everything here happens strictly before update
*/
startScene(){
/* ##### DO NOT MODIFY ##### */
// Create a layer to serve as our main game - Feel free to use this for your own assets
// It is given a depth of 5 to be above our background
this.addLayer("primary", 5);
// Add in the player as an animated sprite
// We give it the key specified in our load function and the name of the layer
this.player = this.add.animatedSprite("player", "primary");
// Set the player's position to the middle of the screen, and scale it down
this.player.position.set(this.viewport.getCenter().x, this.viewport.getCenter().y);
this.player.scale.set(0.5, 0.5);
// Play the idle animation by default
this.player.animation.play("idle");
// Add physics to the player
let playerCollider = new AABB(Vec2.ZERO, new Vec2(64, 64));
// We'll specify a smaller collider centered on the player.
// Also, we don't need collision handling, so disable it.
this.player.addPhysics(playerCollider, Vec2.ZERO, false);
// Add a a playerController to the player
this.player.addAI(SpaceshipPlayerController, {owner: this.player, spawnFleetEventKey: "spawnFleet"});
/* ##### YOUR CODE GOES BELOW THIS LINE ##### */
// Initialize the fleet object pool
// Initialize the mineral object pool
for(let i = 0; i < this.minerals.length; i++){
this.minerals[i] = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(0, 0), size: new Vec2(32, 32)});
this.minerals[i].visible = false;
}
// Initialize the asteroid object pool
let gc = this.add.graphic(GraphicType.RECT, "primary", {position: new Vec2(400, 400), size: new Vec2(100, 100)});
gc.useCustomShader(Homework1Shaders.GRADIENT_CIRCLE);
// Subscribe to events
this.receiver.subscribe(Homework1Event.PLAYER_DAMAGE);
this.receiver.subscribe(Homework1Event.SPAWN_FLEET);
}
/*
* updateScene() is where the real work is done. This is where any custom behavior goes.
*/
updateScene(){
// Handle events we care about
while(this.receiver.hasNextEvent()){
let event = this.receiver.getNextEvent();
}
// Check for collisions
for(let i = 0; i < this.minerals.length; i++){
if(this.player.collisionShape.overlaps(this.minerals[i].boundary)){
console.log(true);
}
}
}
}

View File

@ -0,0 +1,77 @@
import AI from "../Wolfie2D/DataTypes/Interfaces/AI";
import Vec2 from "../Wolfie2D/DataTypes/Vec2";
import Debug from "../Wolfie2D/Debug/Debug";
import Emitter from "../Wolfie2D/Events/Emitter";
import GameEvent from "../Wolfie2D/Events/GameEvent";
import Input from "../Wolfie2D/Input/Input";
import AnimatedSprite from "../Wolfie2D/Nodes/Sprites/AnimatedSprite";
import MathUtils from "../Wolfie2D/Utils/MathUtils";
import { Homework1Event } from "./HW1_Enums";
export default class SpaceshipPlayerController implements AI {
// We want to be able to control our owner, so keep track of them
private owner: AnimatedSprite;
// The direction the spaceship is moving
private direction: Vec2;
private MIN_SPEED: number = 0;
private MAX_SPEED: number = 300;
private speed: number;
private ACCELERATION: number = 4;
private rotationSpeed: number;
// An emitter to hook into the event queue
private emitter: Emitter;
initializeAI(owner: AnimatedSprite, options: Record<string, any>): void {
this.owner = owner;
// Start facing up
this.direction = new Vec2(0, 1);
this.speed = 0;
this.rotationSpeed = 2;
this.emitter = new Emitter();
}
handleEvent(event: GameEvent): void {
// We need to handle animations when we get hurt
if(event.type === Homework1Event.PLAYER_DAMAGE){
this.owner.animation.play("shield");
}
}
update(deltaT: number): void {
// We need to handle player input
let forwardAxis = (Input.isPressed('forward') ? 1 : 0) + (Input.isPressed('backward') ? -1 : 0);
let turnDirection = (Input.isPressed('turn_ccw') ? -1 : 0) + (Input.isPressed('turn_cw') ? 1 : 0);
// Space controls - speed stays the same if nothing happens
// Forward to speed up, backward to slow down
this.speed += this.ACCELERATION * forwardAxis;
this.speed = MathUtils.clamp(this.speed, this.MIN_SPEED, this.MAX_SPEED);
// Rotate the player
this.direction.rotateCCW(turnDirection * this.rotationSpeed * deltaT);
// Update the visual direction of the player
this.owner.rotation = -(Math.atan2(this.direction.y, this.direction.x) - Math.PI/2);
// Move the player with physics
this.owner.move(this.direction.scaled(-this.speed * deltaT));
// If the player clicked, we need to spawn in a fleet member
if(Input.isMouseJustPressed()){
this.emitter.fireEvent(Homework1Event.SPAWN_FLEET, {position: Input.getGlobalMousePosition()});
}
// Animations
if(!this.owner.animation.isPlaying("shield")){
if(this.speed > 0){
this.owner.animation.playIfNotAlready("boost");
} else {
this.owner.animation.playIfNotAlready("idle");
}
}
}
}

View File

@ -1,30 +1,36 @@
import Game from "./Wolfie2D/Loop/Game"; import Game from "./Wolfie2D/Loop/Game";
import Platformer from "./Platformer"; import Homework1_Scene from "./hw1/HW1_Scene";
import Registry from "./Wolfie2D/Registry/Registry";
import { Homework1Shaders } from "./hw1/HW1_Enums";
import GradientCircleShaderType from "./hw1/GradientCircleShaderType";
// The main function is your entrypoint into Wolfie2D. Specify your first scene and any options here. // The main function is your entrypoint into Wolfie2D. Specify your first scene and any options here.
(function main(){ (function main(){
// These are options for initializing the game // Set up options
// Here, we'll set the size of the viewport, color the background, and set up key bindings.
let options = { let options = {
canvasSize: {x: 800, y: 600}, canvasSize: {x: 1200, y: 800},
zoomLevel: 4, clearColor: {r: 0.1, g: 0.1, b: 0.1},
clearColor: {r: 34, g: 32, b: 52},
inputs: [ inputs: [
{ name: "left", keys: ["a"] }, { name: "forward", keys: ["w"] },
{ name: "right", keys: ["d"] }, { name: "backward", keys: ["s"] },
{ name: "jump", keys: ["space", "w"]} { name: "turn_ccw", keys: ["a"] },
] { name: "turn_cw", keys: ["d"] },
],
useWebGL: true,
showDebug: false
} }
// Create our game. This will create all of the systems. // We have a custom shader, so lets add it to the registry and preload it
const demoGame = new Game(options); Registry.shaders.registerAndPreloadItem(
Homework1Shaders.GRADIENT_CIRCLE, // The key of the shader program
GradientCircleShaderType, // The constructor of the shader program
"hw1_assets/shaders/gradient_circle.vshader", // The path to the vertex shader
"hw1_assets/shaders/gradient_circle.fshader"); // the path to the fragment shader
// Run our game. This will start the game loop and get the updates and renders running. // Create a game with the options specified
demoGame.start(); const game = new Game(options);
// For now, we won't specify any scene options. // Start our game
let sceneOptions = {}; game.start();
game.getSceneManager().addScene(Homework1_Scene, {});
// Add our first scene. This will load this scene into the game world.
demoGame.getSceneManager().addScene(Platformer, sceneOptions);
})(); })();