From 4214ef7fd46fe3197ed57028638fa19f42010052 Mon Sep 17 00:00:00 2001 From: Joe Weaver Date: Mon, 15 Feb 2021 19:44:47 -0500 Subject: [PATCH] added webGL support --- .gitignore | 6 + dist/builtin/shaders/point.fshader | 7 + dist/builtin/shaders/point.vshader | 8 + dist/builtin/shaders/rect.fshader | 7 + dist/builtin/shaders/rect.vshader | 7 + dist/builtin/shaders/sprite.fshader | 9 + dist/builtin/shaders/sprite.vshader | 13 + .../shaders/gradient_circle.fshader | 24 ++ .../shaders/gradient_circle.vshader | 11 + .../spritesheets/player_spaceship.json | 145 ++++++++++ .../spritesheets/player_spaceship.png | Bin 0 -> 67903 bytes src/WebGLScene.ts | 48 ++++ src/Wolfie2D/DataTypes/Mat4x4.ts | 167 +++++++++++ .../DataTypes/Rendering/WebGLGameTexture.ts | 5 + .../DataTypes/Rendering/WebGLProgramType.ts | 29 ++ src/Wolfie2D/DataTypes/Vec2.ts | 4 + src/Wolfie2D/Loop/Game.ts | 35 ++- src/Wolfie2D/Loop/GameOptions.ts | 4 + src/Wolfie2D/Nodes/CanvasNode.ts | 21 ++ src/Wolfie2D/Nodes/GameNode.ts | 1 + src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts | 8 + .../Registry/Registries/ShaderRegistry.ts | 98 +++++++ src/Wolfie2D/Registry/Registry.ts | 16 ++ .../Rendering/Animations/AnimationManager.ts | 24 +- .../Rendering/Animations/AnimationTypes.ts | 1 + src/Wolfie2D/Rendering/CanvasRenderer.ts | 2 +- src/Wolfie2D/Rendering/WebGLRenderer.ts | 136 +++++++++ .../Rendering/WebGLRendering/ShaderType.ts | 44 +++ .../ShaderTypes/PointShaderType.ts | 61 ++++ .../ShaderTypes/QuadShaderType.ts | 25 ++ .../ShaderTypes/RectShaderType.ts | 133 +++++++++ .../ShaderTypes/SpriteShaderType.ts | 153 ++++++++++ .../ResourceManager/ResourceManager.ts | 266 +++++++++++++++++- src/Wolfie2D/Scene/SceneManager.ts | 4 +- src/Wolfie2D/SceneGraph/SceneGraphArray.ts | 2 +- src/Wolfie2D/Utils/Color.ts | 21 ++ src/Wolfie2D/Utils/MathUtils.ts | 4 + src/Wolfie2D/Utils/RenderingUtils.ts | 28 ++ src/hw1/GradientCircleShaderType.ts | 68 +++++ src/hw1/HW1_Enums.ts | 8 + src/hw1/HW1_Scene.ts | 107 +++++++ src/hw1/SpaceshipPlayerController.ts | 77 +++++ src/main.ts | 44 +-- 43 files changed, 1841 insertions(+), 40 deletions(-) create mode 100644 dist/builtin/shaders/point.fshader create mode 100644 dist/builtin/shaders/point.vshader create mode 100644 dist/builtin/shaders/rect.fshader create mode 100644 dist/builtin/shaders/rect.vshader create mode 100644 dist/builtin/shaders/sprite.fshader create mode 100644 dist/builtin/shaders/sprite.vshader create mode 100644 dist/hw1_assets/shaders/gradient_circle.fshader create mode 100644 dist/hw1_assets/shaders/gradient_circle.vshader create mode 100644 dist/hw1_assets/spritesheets/player_spaceship.json create mode 100644 dist/hw1_assets/spritesheets/player_spaceship.png create mode 100644 src/WebGLScene.ts create mode 100644 src/Wolfie2D/DataTypes/Mat4x4.ts create mode 100644 src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts create mode 100644 src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts create mode 100644 src/Wolfie2D/Registry/Registries/ShaderRegistry.ts create mode 100644 src/Wolfie2D/Registry/Registry.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRenderer.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts create mode 100644 src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts create mode 100644 src/Wolfie2D/Utils/RenderingUtils.ts create mode 100644 src/hw1/GradientCircleShaderType.ts create mode 100644 src/hw1/HW1_Enums.ts create mode 100644 src/hw1/HW1_Scene.ts create mode 100644 src/hw1/SpaceshipPlayerController.ts diff --git a/.gitignore b/.gitignore index c1721ba..07b29ed 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,12 @@ dist/* # Include the demo_assets folder !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 ### # !dist/assets/ diff --git a/dist/builtin/shaders/point.fshader b/dist/builtin/shaders/point.fshader new file mode 100644 index 0000000..08dd687 --- /dev/null +++ b/dist/builtin/shaders/point.fshader @@ -0,0 +1,7 @@ +precision mediump float; + +uniform vec4 u_Color; + +void main(){ + gl_FragColor = u_Color; +} \ No newline at end of file diff --git a/dist/builtin/shaders/point.vshader b/dist/builtin/shaders/point.vshader new file mode 100644 index 0000000..bd147d4 --- /dev/null +++ b/dist/builtin/shaders/point.vshader @@ -0,0 +1,8 @@ +attribute vec4 a_Position; + +uniform float u_PointSize; + +void main(){ + gl_Position = a_Position; + gl_PointSize = u_PointSize; +} \ No newline at end of file diff --git a/dist/builtin/shaders/rect.fshader b/dist/builtin/shaders/rect.fshader new file mode 100644 index 0000000..08dd687 --- /dev/null +++ b/dist/builtin/shaders/rect.fshader @@ -0,0 +1,7 @@ +precision mediump float; + +uniform vec4 u_Color; + +void main(){ + gl_FragColor = u_Color; +} \ No newline at end of file diff --git a/dist/builtin/shaders/rect.vshader b/dist/builtin/shaders/rect.vshader new file mode 100644 index 0000000..24c7378 --- /dev/null +++ b/dist/builtin/shaders/rect.vshader @@ -0,0 +1,7 @@ +attribute vec4 a_Position; + +uniform mat4 u_Transform; + +void main(){ + gl_Position = u_Transform * a_Position; +} \ No newline at end of file diff --git a/dist/builtin/shaders/sprite.fshader b/dist/builtin/shaders/sprite.fshader new file mode 100644 index 0000000..a4fb440 --- /dev/null +++ b/dist/builtin/shaders/sprite.fshader @@ -0,0 +1,9 @@ +precision mediump float; + +uniform sampler2D u_Sampler; + +varying vec2 v_TexCoord; + +void main(){ + gl_FragColor = texture2D(u_Sampler, v_TexCoord); +} \ No newline at end of file diff --git a/dist/builtin/shaders/sprite.vshader b/dist/builtin/shaders/sprite.vshader new file mode 100644 index 0000000..f04fc3c --- /dev/null +++ b/dist/builtin/shaders/sprite.vshader @@ -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; +} \ No newline at end of file diff --git a/dist/hw1_assets/shaders/gradient_circle.fshader b/dist/hw1_assets/shaders/gradient_circle.fshader new file mode 100644 index 0000000..3cd2e62 --- /dev/null +++ b/dist/hw1_assets/shaders/gradient_circle.fshader @@ -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); +} \ No newline at end of file diff --git a/dist/hw1_assets/shaders/gradient_circle.vshader b/dist/hw1_assets/shaders/gradient_circle.vshader new file mode 100644 index 0000000..9cb98c8 --- /dev/null +++ b/dist/hw1_assets/shaders/gradient_circle.vshader @@ -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; +} \ No newline at end of file diff --git a/dist/hw1_assets/spritesheets/player_spaceship.json b/dist/hw1_assets/spritesheets/player_spaceship.json new file mode 100644 index 0000000..9ae8470 --- /dev/null +++ b/dist/hw1_assets/spritesheets/player_spaceship.json @@ -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 + } + ] + } + ] +} \ No newline at end of file diff --git a/dist/hw1_assets/spritesheets/player_spaceship.png b/dist/hw1_assets/spritesheets/player_spaceship.png new file mode 100644 index 0000000000000000000000000000000000000000..4b0f48267ad445bfd11856053df2f92a3d2489b9 GIT binary patch literal 67903 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 z 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 Bs{Q}~ literal 0 HcmV?d00001 diff --git a/src/WebGLScene.ts b/src/WebGLScene.ts new file mode 100644 index 0000000..d95cb02 --- /dev/null +++ b/src/WebGLScene.ts @@ -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 = 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; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Mat4x4.ts b/src/Wolfie2D/DataTypes/Mat4x4.ts new file mode 100644 index 0000000..c0765bb --- /dev/null +++ b/src/Wolfie2D/DataTypes/Mat4x4.ts @@ -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): 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 { + // 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)}|`; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts b/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts new file mode 100644 index 0000000..c111c66 --- /dev/null +++ b/src/Wolfie2D/DataTypes/Rendering/WebGLGameTexture.ts @@ -0,0 +1,5 @@ +export default class WebGLGameTexture { + webGLTextureId: number; + webGLTexture: WebGLTexture; + imageKey: string; +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts b/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts new file mode 100644 index 0000000..6452dfa --- /dev/null +++ b/src/Wolfie2D/DataTypes/Rendering/WebGLProgramType.ts @@ -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); + } + } +} \ No newline at end of file diff --git a/src/Wolfie2D/DataTypes/Vec2.ts b/src/Wolfie2D/DataTypes/Vec2.ts index b618781..10783a9 100644 --- a/src/Wolfie2D/DataTypes/Vec2.ts +++ b/src/Wolfie2D/DataTypes/Vec2.ts @@ -383,6 +383,10 @@ export default class Vec2 { this.onChange = f; } + toArray(): Float32Array { + return this.vec; + } + /** * Performs linear interpolation between two vectors * @param a The first vector diff --git a/src/Wolfie2D/Loop/Game.ts b/src/Wolfie2D/Loop/Game.ts index b5bc04b..47b174c 100644 --- a/src/Wolfie2D/Loop/Game.ts +++ b/src/Wolfie2D/Loop/Game.ts @@ -16,6 +16,8 @@ import GameLoop from "./GameLoop"; import FixedUpdateGameLoop from "./FixedUpdateGameLoop"; import EnvironmentInitializer from "./EnvironmentInitializer"; import Vec2 from "../DataTypes/Vec2"; +import Registry from "../Registry/Registry"; +import WebGLRenderer from "../Rendering/WebGLRenderer"; /** * The main loop of the game engine. @@ -36,7 +38,7 @@ export default class Game { readonly WIDTH: number; readonly HEIGHT: number; private viewport: Viewport; - private ctx: CanvasRenderingContext2D; + private ctx: CanvasRenderingContext2D | WebGLRenderingContext; private clearColor: Color; // 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.HEIGHT = this.gameOptions.canvasSize.y; - // For now, just hard code a canvas renderer. We can do this with options later - this.renderingManager = new CanvasRenderer(); + // This step MUST happen before the resource manager does anything + if(this.gameOptions.useWebGL){ + this.renderingManager = new WebGLRenderer(); + } else { + this.renderingManager = new CanvasRenderer(); + } this.initializeGameWindow(); 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); @@ -131,8 +137,14 @@ export default class Game { // Set the render function of the loop this.loop.doRender = () => this.render(); - // Start the loop - this.loop.start(); + // 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(); + }); } /** @@ -164,12 +176,17 @@ export default class Game { */ render(): void { // Clear the canvases - this.ctx.clearRect(0, 0, this.WIDTH, this.HEIGHT); Debug.clearCanvas(); - // Game Canvas - this.ctx.fillStyle = this.clearColor.toString(); - this.ctx.fillRect(0, 0, this.WIDTH, this.HEIGHT); + if(this.gameOptions.useWebGL){ + (this.ctx).clearColor(this.clearColor.r, this.clearColor.g, this.clearColor.b, this.clearColor.a); + (this.ctx).clear((this.ctx).COLOR_BUFFER_BIT | (this.ctx).DEPTH_BUFFER_BIT); + } else { + (this.ctx).clearRect(0, 0, this.WIDTH, this.HEIGHT); + (this.ctx).fillStyle = this.clearColor.toString(); + (this.ctx).fillRect(0, 0, this.WIDTH, this.HEIGHT); + } + this.sceneManager.render(); // Debug render diff --git a/src/Wolfie2D/Loop/GameOptions.ts b/src/Wolfie2D/Loop/GameOptions.ts index 50bd213..a9112fb 100644 --- a/src/Wolfie2D/Loop/GameOptions.ts +++ b/src/Wolfie2D/Loop/GameOptions.ts @@ -20,6 +20,9 @@ export default class GameOptions { /* Whether or not the stats rendering should occur */ showStats: boolean; + /* Whether or not to use webGL */ + useWebGL: boolean; + /** * Parses the data in the raw options object * @param options The game options as a Record @@ -34,6 +37,7 @@ export default class GameOptions { gOpt.inputs = options.inputs ? options.inputs : []; gOpt.showDebug = !!options.showDebug; gOpt.showStats = !!options.showStats; + gOpt.useWebGL = !!options.useWebGL; return gOpt; } diff --git a/src/Wolfie2D/Nodes/CanvasNode.ts b/src/Wolfie2D/Nodes/CanvasNode.ts index b4d4f30..59eb0a3 100644 --- a/src/Wolfie2D/Nodes/CanvasNode.ts +++ b/src/Wolfie2D/Nodes/CanvasNode.ts @@ -12,6 +12,8 @@ export default abstract class CanvasNode extends GameNode implements Region { private _size: Vec2; private _scale: Vec2; private _boundary: AABB; + private _hasCustomShader: boolean; + private _customShaderKey: string; /** A flag for whether or not the CanvasNode is visible */ visible: boolean = true; @@ -24,6 +26,8 @@ export default abstract class CanvasNode extends GameNode implements Region { this._scale.setOnChange(() => this.scaleChanged()); this._boundary = new AABB(); this.updateBoundary(); + + this._hasCustomShader = false; } get size(): Vec2 { @@ -56,6 +60,14 @@ export default abstract class CanvasNode extends GameNode implements Region { this.scale.y = value; } + get hasCustomShader(): boolean { + return this._hasCustomShader; + } + + get customShaderKey(): string { + return this._customShaderKey; + } + // @override protected positionChanged(): void { super.positionChanged(); @@ -89,6 +101,15 @@ export default abstract class CanvasNode extends GameNode implements Region { 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 * @param x The x position of the point diff --git a/src/Wolfie2D/Nodes/GameNode.ts b/src/Wolfie2D/Nodes/GameNode.ts index 51854b7..260a687 100644 --- a/src/Wolfie2D/Nodes/GameNode.ts +++ b/src/Wolfie2D/Nodes/GameNode.ts @@ -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. if(collisionShape){ this.collisionShape = collisionShape; + this.collisionShape.center = this.position; } else if (isRegion(this)) { // If the gamenode has a region and no other is specified, use that this.collisionShape = (this).boundary.clone(); diff --git a/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts b/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts index ae4ffef..bdd909f 100644 --- a/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts +++ b/src/Wolfie2D/Nodes/Sprites/AnimatedSprite.ts @@ -8,9 +8,17 @@ export default class AnimatedSprite extends Sprite { /** The number of columns in this sprite sheet */ protected numCols: number; + get cols(): number { + return this.numCols; + } + /** The number of rows in this sprite sheet */ protected numRows: number; + get rows(): number { + return this.numRows; + } + /** The animationManager for this sprite */ animation: AnimationManager; diff --git a/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts b/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts new file mode 100644 index 0000000..33d4636 --- /dev/null +++ b/src/Wolfie2D/Registry/Registries/ShaderRegistry.ts @@ -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 { + + // Shader names + public static POINT_SHADER = "point"; + public static RECT_SHADER = "rect"; + public static SPRITE_SHADER = "sprite"; + + private registryItems: Array = 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; +} \ No newline at end of file diff --git a/src/Wolfie2D/Registry/Registry.ts b/src/Wolfie2D/Registry/Registry.ts new file mode 100644 index 0000000..113fb51 --- /dev/null +++ b/src/Wolfie2D/Registry/Registry.ts @@ -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(); + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/Animations/AnimationManager.ts b/src/Wolfie2D/Rendering/Animations/AnimationManager.ts index 05dfc6b..18dc2f4 100644 --- a/src/Wolfie2D/Rendering/Animations/AnimationManager.ts +++ b/src/Wolfie2D/Rendering/Animations/AnimationManager.ts @@ -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 * @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 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){ 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 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.currentFrame = 0; this.frameProgress = 0; - this.loop = loop; 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){ this.onEndEvent = onEnd; } else { this.onEndEvent = null; } + // Reset pending animation this.pendingAnimation = null; } diff --git a/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts b/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts index e96f7bd..72f0ac7 100644 --- a/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts +++ b/src/Wolfie2D/Rendering/Animations/AnimationTypes.ts @@ -12,6 +12,7 @@ export enum AnimationState { export class AnimationData { name: string; frames: Array<{index: number, duration: number}>; + repeat: boolean = false; } export class TweenData { diff --git a/src/Wolfie2D/Rendering/CanvasRenderer.ts b/src/Wolfie2D/Rendering/CanvasRenderer.ts index caa1cb5..6a72b69 100644 --- a/src/Wolfie2D/Rendering/CanvasRenderer.ts +++ b/src/Wolfie2D/Rendering/CanvasRenderer.ts @@ -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.rotate(node.rotation); + this.ctx.rotate(-node.rotation); let globalAlpha = this.ctx.globalAlpha; this.ctx.globalAlpha = node.alpha; diff --git a/src/Wolfie2D/Rendering/WebGLRenderer.ts b/src/Wolfie2D/Rendering/WebGLRenderer.ts new file mode 100644 index 0000000..1e91d09 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRenderer.ts @@ -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): 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); + } + +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts new file mode 100644 index 0000000..897dc09 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderType.ts @@ -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): 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 {return {};} +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts new file mode 100644 index 0000000..e7ae466 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/PointShaderType.ts @@ -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): 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 { + let options: Record = { + position: point.position, + color: point.color, + pointSize: point.size, + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts new file mode 100644 index 0000000..fa524f1 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/QuadShaderType.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts new file mode 100644 index 0000000..e977434 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/RectShaderType.ts @@ -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): 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 { + let options: Record = { + position: rect.position, + color: rect.color, + size: rect.size, + rotation: rect.rotation + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts new file mode 100644 index 0000000..3753745 --- /dev/null +++ b/src/Wolfie2D/Rendering/WebGLRendering/ShaderTypes/SpriteShaderType.ts @@ -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): 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 { + 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 = { + position: sprite.position, + rotation: sprite.rotation, + size: sprite.size, + scale: sprite.scale.toArray(), + imageKey: sprite.imageId, + texShift, + texScale + } + + return options; + } +} \ No newline at end of file diff --git a/src/Wolfie2D/ResourceManager/ResourceManager.ts b/src/Wolfie2D/ResourceManager/ResourceManager.ts index 3d00d47..6532496 100644 --- a/src/Wolfie2D/ResourceManager/ResourceManager.ts +++ b/src/Wolfie2D/ResourceManager/ResourceManager.ts @@ -4,6 +4,8 @@ import { TiledTilemapData } from "../DataTypes/Tilesets/TiledData"; import StringUtils from "../Utils/StringUtils"; import AudioManager from "../Sound/AudioManager"; import Spritesheet from "../DataTypes/Spritesheet"; +import WebGLProgramType from "../DataTypes/Rendering/WebGLProgramType"; +import PhysicsManager from "../Physics/PhysicsManager"; /** * 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) */ 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; + + private gl_DefaultShaderPrograms: Map; + private gl_ShaderPrograms: Map; + + private gl_Textures: Map; + private gl_Buffers: Map; + + private gl: WebGLRenderingContext; + private constructor(){ this.loading = false; this.justLoaded = false; @@ -91,6 +108,16 @@ export default class ResourceManager { this.loadonly_audioToLoad = 0; this.loadonly_audioLoadingQueue = new Queue(); 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; } + /** + * 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 * @param key The key to associate the loaded image with @@ -199,15 +239,26 @@ export default class ResourceManager { console.log("Loaded Images"); this.loadAudioFromQueue(() => { console.log("Loaded Audio"); - // Done loading - this.loading = false; - this.justLoaded = true; - callback(); + + 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 + this.loading = false; + this.justLoaded = true; + callback(); } /** @@ -232,6 +283,12 @@ export default class ResourceManager { this.loadonly_audioLoaded = 0; this.loadonly_audioToLoad = 0; 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 this.images.add(key, image); + // If WebGL is active, create a texture + this.createWebGLTexture(key); + // Finish image load 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 { let xobj: XMLHttpRequest = new XMLHttpRequest(); xobj.overrideMimeType("application/json"); @@ -476,6 +722,8 @@ export default class ResourceManager { xobj.send(null); } + /* ########## LOADING BAR INFO ########## */ + private getLoadPercent(): number { return (this.loadonly_tilemapsLoaded/this.loadonly_tilemapsToLoad + this.loadonly_spritesheetsLoaded/this.loadonly_spritesheetsToLoad @@ -499,6 +747,12 @@ export default class ResourceManager { } class KeyPathPair { - key: string - path: string + key: string; + path: string; +} + +class KeyPath_Shader { + key: string; + vpath: string; + fpath: string; } \ No newline at end of file diff --git a/src/Wolfie2D/Scene/SceneManager.ts b/src/Wolfie2D/Scene/SceneManager.ts index e8bccc7..331c07c 100644 --- a/src/Wolfie2D/Scene/SceneManager.ts +++ b/src/Wolfie2D/Scene/SceneManager.ts @@ -88,7 +88,9 @@ export default class SceneManager { * Renders the current Scene */ public render(): void { - this.currentScene.render(); + if(this.currentScene.isRunning()){ + this.currentScene.render(); + } } /** diff --git a/src/Wolfie2D/SceneGraph/SceneGraphArray.ts b/src/Wolfie2D/SceneGraph/SceneGraphArray.ts index c47b4b9..c9fa6e3 100644 --- a/src/Wolfie2D/SceneGraph/SceneGraphArray.ts +++ b/src/Wolfie2D/SceneGraph/SceneGraphArray.ts @@ -83,7 +83,7 @@ export default class SceneGraphArray extends SceneGraph { let visibleSet = new Array(); 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); } } diff --git a/src/Wolfie2D/Utils/Color.ts b/src/Wolfie2D/Utils/Color.ts index 55096a5..8f936b0 100644 --- a/src/Wolfie2D/Utils/Color.ts +++ b/src/Wolfie2D/Utils/Color.ts @@ -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); } + /** + * 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 #RRGGBB @@ -164,4 +172,17 @@ export default class Color { } 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 + ]); + } } \ No newline at end of file diff --git a/src/Wolfie2D/Utils/MathUtils.ts b/src/Wolfie2D/Utils/MathUtils.ts index a910b18..1b4c0c8 100644 --- a/src/Wolfie2D/Utils/MathUtils.ts +++ b/src/Wolfie2D/Utils/MathUtils.ts @@ -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 * @param a The first value for the interpolation bound diff --git a/src/Wolfie2D/Utils/RenderingUtils.ts b/src/Wolfie2D/Utils/RenderingUtils.ts new file mode 100644 index 0000000..f822227 --- /dev/null +++ b/src/Wolfie2D/Utils/RenderingUtils.ts @@ -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 + ]); + } +} \ No newline at end of file diff --git a/src/hw1/GradientCircleShaderType.ts b/src/hw1/GradientCircleShaderType.ts new file mode 100644 index 0000000..adb6db0 --- /dev/null +++ b/src/hw1/GradientCircleShaderType.ts @@ -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): 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); + } +} \ No newline at end of file diff --git a/src/hw1/HW1_Enums.ts b/src/hw1/HW1_Enums.ts new file mode 100644 index 0000000..d380546 --- /dev/null +++ b/src/hw1/HW1_Enums.ts @@ -0,0 +1,8 @@ +export enum Homework1Event { + PLAYER_DAMAGE = "PLAYER_DAMAGE", + SPAWN_FLEET = "SPAWN_FLEET" +} + +export enum Homework1Shaders { + GRADIENT_CIRCLE = "GRADIENT_CIRCLE" +} \ No newline at end of file diff --git a/src/hw1/HW1_Scene.ts b/src/hw1/HW1_Scene.ts new file mode 100644 index 0000000..abba8e3 --- /dev/null +++ b/src/hw1/HW1_Scene.ts @@ -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 = new Array(this.MAX_FLEET_SIZE); + + // Create an object pool for our fleet + private MAX_NUM_ASTEROIDS = 6; + private asteroids: Array = new Array(this.MAX_NUM_ASTEROIDS); + + // Create an object pool for our fleet + private MAX_NUM_MINERALS = 20; + private minerals: Array = 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); + } + } + } +} \ No newline at end of file diff --git a/src/hw1/SpaceshipPlayerController.ts b/src/hw1/SpaceshipPlayerController.ts new file mode 100644 index 0000000..e4a3ec0 --- /dev/null +++ b/src/hw1/SpaceshipPlayerController.ts @@ -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): 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"); + } + } + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index d5c2cc3..6acbaa2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,30 +1,36 @@ 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. (function main(){ - // These are options for initializing the game - // Here, we'll set the size of the viewport, color the background, and set up key bindings. + // Set up options let options = { - canvasSize: {x: 800, y: 600}, - zoomLevel: 4, - clearColor: {r: 34, g: 32, b: 52}, + canvasSize: {x: 1200, y: 800}, + clearColor: {r: 0.1, g: 0.1, b: 0.1}, inputs: [ - { name: "left", keys: ["a"] }, - { name: "right", keys: ["d"] }, - { name: "jump", keys: ["space", "w"]} - ] + { name: "forward", keys: ["w"] }, + { name: "backward", keys: ["s"] }, + { name: "turn_ccw", keys: ["a"] }, + { name: "turn_cw", keys: ["d"] }, + ], + useWebGL: true, + showDebug: false } - // Create our game. This will create all of the systems. - const demoGame = new Game(options); + // We have a custom shader, so lets add it to the registry and preload it + 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. - demoGame.start(); + // Create a game with the options specified + const game = new Game(options); - // For now, we won't specify any scene options. - let sceneOptions = {}; - - // Add our first scene. This will load this scene into the game world. - demoGame.getSceneManager().addScene(Platformer, sceneOptions); + // Start our game + game.start(); + game.getSceneManager().addScene(Homework1_Scene, {}); })(); \ No newline at end of file