From c846042597c17b602278cca5577f2f3b2b6ca309 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Mon, 16 Sep 2019 21:18:01 +0200 Subject: [PATCH] wtf --- backend/src/main.rs | 2 +- backend/src/planetwars/mod.rs | 17 +- client/Cargo.toml | 8 + client/src/main.rs | 289 ++++++++++++++++++++- frontend/www/index.js | 1 + frontend/www/package-lock.json | 79 +++++- frontend/www/package.json | 9 +- frontend/www/tsconfig.json | 15 ++ frontend/www/webgl/buffer.ts | 55 ++++ frontend/www/webgl/instance.ts | 72 ++++++ frontend/www/webgl/renderer.ts | 72 ++++++ frontend/www/webgl/shader.ts | 305 +++++++++++++++++++++++ frontend/www/webgl/texture.ts | 68 +++++ frontend/www/webgl/util.ts | 129 ++++++++++ frontend/www/webgl/vertexArray.ts | 0 frontend/www/webgl/vertexBufferLayout.ts | 112 +++++++++ frontend/www/webpack.config.js | 22 +- 17 files changed, 1239 insertions(+), 16 deletions(-) create mode 100644 frontend/www/tsconfig.json create mode 100644 frontend/www/webgl/buffer.ts create mode 100644 frontend/www/webgl/instance.ts create mode 100644 frontend/www/webgl/renderer.ts create mode 100644 frontend/www/webgl/shader.ts create mode 100644 frontend/www/webgl/texture.ts create mode 100644 frontend/www/webgl/util.ts create mode 100644 frontend/www/webgl/vertexArray.ts create mode 100644 frontend/www/webgl/vertexBufferLayout.ts diff --git a/backend/src/main.rs b/backend/src/main.rs index cb9b5ab..8dcb2ae 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -43,7 +43,7 @@ pub fn run(args : Vec) { let ids: HashMap = (0..number_of_clients).map(|x| (rand::thread_rng().gen::().into(), x.into())).collect(); - let config = planetwars::Config { map_file: String::from("map.json"), max_turns: 500 }; + let config = planetwars::Config { map_file: String::from("hex.json"), max_turns: 500 }; let game = planetwars::PlanetWarsGame::new(config.create_game(number_of_clients as usize)); println!("Tokens:"); diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs index fe195e3..3f02d4b 100644 --- a/backend/src/planetwars/mod.rs +++ b/backend/src/planetwars/mod.rs @@ -5,6 +5,8 @@ use serde_json; use std::collections::HashMap; use std::convert::TryInto; +use std::fs::File; +use std::io::Write; mod pw_config; mod pw_serializer; @@ -17,25 +19,30 @@ pub use pw_config::Config; pub struct PlanetWarsGame { state: pw_rules::PlanetWars, planet_map: HashMap, + log_file: File } impl PlanetWarsGame { pub fn new(state: pw_rules::PlanetWars) -> Self { let planet_map = state.planets.iter().map(|p| (p.name.clone(), p.id)).collect(); + let file = File::create("game.json").unwrap(); Self { - state, planet_map + state, planet_map, + log_file: file, } } - fn dispatch_state(&self, were_alive: Vec, updates: &mut Vec, ) { + fn dispatch_state(&mut self, were_alive: Vec, updates: &mut Vec, ) { let state = pw_serializer::serialize(&self.state); - println!("{}", serde_json::to_string(&state).unwrap()); + write!(self.log_file, "{}\n", serde_json::to_string(&state).unwrap()).unwrap(); + + // println!("{}", serde_json::to_string(&state).unwrap()); for player in self.state.players.iter().filter(|p| were_alive.contains(&p.id)) { let state = pw_serializer::serialize_rotated(&self.state, player.id); - let state = if player.alive { + let state = if player.alive && !self.state.is_finished() { proto::ServerMessage::GameState(state) } else { proto::ServerMessage::FinalState(state) @@ -45,7 +52,7 @@ impl PlanetWarsGame { game::Update::Player((player.id as u64).into(), serde_json::to_vec(&state).unwrap()) ); - if !player.alive { + if !player.alive || self.state.is_finished() { updates.push(game::Update::Kick((player.id as u64).into())); } } diff --git a/client/Cargo.toml b/client/Cargo.toml index 594bb4a..508e5e1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -7,3 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +mozaic = { git = "https://github.com/ajuvercr/MOZAIC" } +tokio = "0.1.22" +capnp = "0.10.1" +futures = "0.1.28" +serde = "1.0.100" +serde_derive = "1.0.100" +serde_json = "1.0" +rand = { version = "0.6.5", default-features = true } diff --git a/client/src/main.rs b/client/src/main.rs index e7a11a9..fbc15d5 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,3 +1,290 @@ +extern crate mozaic; +extern crate tokio; +extern crate futures; +extern crate capnp; +extern crate rand; + +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +use rand::Rng; + +use mozaic::core_capnp::{initialize, terminate_stream, identify, actor_joined}; +use mozaic::messaging::reactor::*; +use mozaic::messaging::types::*; +use mozaic::errors::*; +use mozaic::client_capnp::{client_message, host_message, client_kicked}; +use mozaic::mozaic_cmd_capnp::{bot_input, bot_return}; +use mozaic::server::runtime::{Broker, BrokerHandle}; +use mozaic::server; +use mozaic::modules::{BotReactor}; + +use std::env; +use std::str; + +mod types; + fn main() { - println!("Hello, world!"); + let args: Vec = env::args().collect(); + let id = args.get(1).unwrap().parse().unwrap(); + let client_args = args.get(2..).expect("How do you expect me to spawn your bot?").to_vec(); + + let addr = "127.0.0.1:9142".parse().unwrap(); + let self_id: ReactorId = rand::thread_rng().gen(); + + tokio::run(futures::lazy(move || { + let mut broker = Broker::new().unwrap(); + let reactor = ClientReactor { + server: None, + id, + broker: broker.clone(), + args: client_args, + }; + broker.spawn(self_id.clone(), reactor.params(), "main").display(); + + tokio::spawn(server::connect_to_server(broker, self_id, &addr)); + Ok(()) + })); +} + +// Main client logic +/// ? greeter_id is the server, the tcp stream that you connected to +/// ? runtime_id is your own runtime, to handle visualisation etc +/// ? user are you ... +struct ClientReactor { + server: Option, + broker: BrokerHandle, + id: u64, + args: Vec, +} + +impl ClientReactor { + fn params(self) -> CoreParams { + let mut params = CoreParams::new(self); + params.handler(initialize::Owned, CtxHandler::new(Self::initialize)); + params.handler(actor_joined::Owned, CtxHandler::new(Self::open_host)); + return params; + } + + // reactor setup + fn initialize( + &mut self, + handle: &mut ReactorHandle, + _: initialize::Reader, + ) -> Result<()> + { + // open link with runtime, for communicating with chat GUI + let runtime_link = RuntimeLink::params(handle.id().clone()); + handle.open_link(runtime_link)?; + + let bot = BotReactor::new(self.broker.clone(), handle.id().clone(), self.args.clone()); + let bot_id = handle.spawn(bot.params(), "Bot Driver")?; + + handle.open_link(BotLink::params(bot_id))?; + + return Ok(()); + } + + fn open_host( + &mut self, + handle: &mut ReactorHandle, + r: actor_joined::Reader, + ) -> Result<()> + { + let id = r.get_id()?; + + if let Some(server) = &self.server { + handle.open_link(HostLink::params(ReactorId::from(id)))?; + self.broker.register_as(id.into(), server.clone()); + + // Fake bot msg + let mut chat_message = MsgBuffer::::new(); + chat_message.build(|b| { + b.set_message(b""); + }); + handle.send_internal(chat_message)?; + + } else { + + handle.open_link(ServerLink::params(id.into()))?; + self.server = Some(id.into()); + + let mut identify = MsgBuffer::::new(); + identify.build(|b| { + b.set_key(self.id); + }); + handle.send_internal(identify).display(); + + } + + Ok(()) + } +} + +// Handler for the connection with the chat server +struct ServerLink; +impl ServerLink { + fn params(foreign_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(foreign_id, Self); + + params.external_handler( terminate_stream::Owned, CtxHandler::new(Self::close_handler) ); + params.external_handler( actor_joined::Owned, CtxHandler::new(actor_joined::e_to_i) ); + + params.internal_handler( identify::Owned, CtxHandler::new(Self::identify) ); + + return params; + } + + fn identify( + &mut self, + handle: &mut LinkHandle, + id: identify::Reader, + ) -> Result<()> { + let id = id.get_key(); + + let mut chat_message = MsgBuffer::::new(); + chat_message.build(|b| { + b.set_key(id); + }); + + handle.send_message(chat_message).display(); + Ok(()) + } + + fn close_handler( + &mut self, + handle: &mut LinkHandle, + _: terminate_stream::Reader, + ) -> Result<()> + { + // also close our end of the stream + handle.close_link()?; + return Ok(()); + } +} + +struct HostLink; +impl HostLink { + fn params(remote_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(remote_id, HostLink); + + params.external_handler( + host_message::Owned, + CtxHandler::new(Self::receive_host_message), + ); + + params.internal_handler( + bot_return::Owned, + CtxHandler::new(Self::send_chat_message), + ); + + params.external_handler( + client_kicked::Owned, + CtxHandler::new(Self::client_kicked), + ); + + return params; + } + + // pick up a 'send_message' event from the reactor, and put it to effect + // by constructing the chat message and sending it to the chat server. + fn send_chat_message( + &mut self, + handle: &mut LinkHandle, + send_message: bot_return::Reader, + ) -> Result<()> + { + let message = send_message.get_message()?; + + println!("Our bot sent"); + println!("{}", str::from_utf8(&message).unwrap()); + + let mut chat_message = MsgBuffer::::new(); + chat_message.build(|b| { + b.set_data(message); + }); + + handle.send_message(chat_message)?; + + return Ok(()); + } + + // pick up a 'send_message' event from the reactor, and put it to effect + // by constructing the chat message and sending it to the chat server. + fn client_kicked( + &mut self, + handle: &mut LinkHandle, + _: client_kicked::Reader, + ) -> Result<()> + { + // Disconnect + + handle.close_link()?; + + return Ok(()); + } + + // receive a chat message from the chat server, and broadcast it on the + // reactor. + fn receive_host_message( + &mut self, + handle: &mut LinkHandle, + host_message: host_message::Reader, + ) -> Result<()> + { + let message = host_message.get_data()?; + + let message: types::ServerMessage = serde_json::from_slice(message).unwrap(); + + println!(""); + match message { + types::ServerMessage::GameState(state) => { + // println!("New game state"); + let mut bot_msg = MsgBuffer::::new(); + + bot_msg.build(|b| { + b.set_input(&serde_json::to_vec(&state).unwrap()); + }); + handle.send_internal(bot_msg).display(); + }, + types::ServerMessage::FinalState(state) => { + println!("Game finished with"); + println!("{:?}", state); + } + types::ServerMessage::PlayerAction(action) => { + println!("Out bot did"); + println!("{:?}", action); + } + } + + return Ok(()); + } +} + +struct BotLink; +impl BotLink { + fn params(foreign_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(foreign_id, Self); + + params.external_handler(bot_return::Owned, CtxHandler::new(bot_return::e_to_i)); + params.internal_handler(bot_input::Owned, CtxHandler::new(bot_input::i_to_e)); + + return params; + } +} + +struct RuntimeLink; +impl RuntimeLink { + fn params(foreign_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(foreign_id, Self); + + params.external_handler( + actor_joined::Owned, + CtxHandler::new(actor_joined::e_to_i), + ); + + return params; + } } diff --git a/frontend/www/index.js b/frontend/www/index.js index a7c06fa..dcc5aa1 100644 --- a/frontend/www/index.js +++ b/frontend/www/index.js @@ -1,5 +1,6 @@ import { Game } from "planetwars"; import { memory } from "planetwars/plantwars_bg" +import { Shader } from "./webgl/shader" const URL = window.location.origin+window.location.pathname; const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); diff --git a/frontend/www/package-lock.json b/frontend/www/package-lock.json index e681255..19fdad9 100644 --- a/frontend/www/package-lock.json +++ b/frontend/www/package-lock.json @@ -4063,6 +4063,12 @@ "sha.js": "^2.4.8" } }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -4094,8 +4100,7 @@ } }, "planetwars": { - "version": "file:../pkg", - "dev": true + "version": "file:../pkg" }, "portfinder": { "version": "1.0.21", @@ -5192,6 +5197,70 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, + "ts-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.1.0.tgz", + "integrity": "sha512-7JedeOu2rsYHQDEr2fwmMozABwbQTZXEaEMZPSIWG7gpzRefOLJCqwdazcegHtyaxp04PeEgs/b0m08WMpnIzQ==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -5220,6 +5289,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", + "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "dev": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/frontend/www/package.json b/frontend/www/package.json index e287e37..46e25a6 100644 --- a/frontend/www/package.json +++ b/frontend/www/package.json @@ -2,7 +2,8 @@ "name": "create-wasm-app", "version": "0.1.0", "description": "create an app to consume rust-generated wasm packages", - "main": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "webpack --config webpack.config.js", "start": "webpack-dev-server" @@ -18,6 +19,9 @@ "webpack", "mozaic" ], + "dependencies": { + "planetwars": "file:../pkg" + }, "author": "Arthur Vercruysse ", "license": "(MIT OR Apache-2.0)", "bugs": { @@ -25,8 +29,9 @@ }, "homepage": "https://github.com/ajuvercr/Planetwars#Readme", "devDependencies": { - "planetwars": "file:../pkg", "webpack": "^4.29.3", + "ts-loader": "^6.0.2", + "typescript": "^3.5.2", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.5", "copy-webpack-plugin": "^5.0.0" diff --git a/frontend/www/tsconfig.json b/frontend/www/tsconfig.json new file mode 100644 index 0000000..f5c8a2e --- /dev/null +++ b/frontend/www/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["es2017", "es7", "es6", "dom"], + "outDir": "./dist/", + "noImplicitAny": false, + "module": "commonjs", + "target": "es6", + "jsx": "react", + "declaration": true + }, + "exclude": [ + "node_modules", + "dist" + ] + } diff --git a/frontend/www/webgl/buffer.ts b/frontend/www/webgl/buffer.ts new file mode 100644 index 0000000..2739fbe --- /dev/null +++ b/frontend/www/webgl/buffer.ts @@ -0,0 +1,55 @@ + +export class Buffer { + buffer: WebGLBuffer; + data: any; + count: number; + type: number; + + constructor(gl: WebGLRenderingContext, data: number[], type: number) { + this.buffer = gl.createBuffer(); + this.type = type; + + if (data) + this.updateData(gl, data); + } + + _toArray(data: number[]): any { + return new Float32Array(data); + } + + updateData(gl: WebGLRenderingContext, data: number[]) { + this.data = data; + this.count = data.length; + gl.bindBuffer(this.type, this.buffer); + gl.bufferData(this.type, this._toArray(data), gl.STATIC_DRAW); + } + + bind(gl: WebGLRenderingContext) { + gl.bindBuffer(this.type, this.buffer); + } + + getCount(): number { + return this.count; + } +} + +export class VertexBuffer extends Buffer { + constructor(gl: WebGLRenderingContext, data: any) { + super(gl, data, gl.ARRAY_BUFFER); + } + + _toArray(data: number[]): any { + return new Float32Array(data); + } +} + + +export class IndexBuffer extends Buffer { + constructor(gl: WebGLRenderingContext, data: any) { + super(gl, data, gl.ELEMENT_ARRAY_BUFFER); + } + + _toArray(data: number[]): any { + return new Uint16Array(data); + } +} diff --git a/frontend/www/webgl/instance.ts b/frontend/www/webgl/instance.ts new file mode 100644 index 0000000..ec85144 --- /dev/null +++ b/frontend/www/webgl/instance.ts @@ -0,0 +1,72 @@ +import { Renderable } from './renderer'; +import { Shader, Uniform } from './shader'; +import { Dictionary } from './util'; + +function createAndSetupTexture(gl: WebGLRenderingContext): WebGLTexture { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Set up texture so we can render any size image and so we are + // working with pixels. + 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); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + return texture; +} + +export class Foo implements Renderable { + stages: Stage[]; + + textures: WebGLTexture[]; + framebuffers: WebGLFramebuffer[]; + + width: number; + height: number; + + constructor(gl: WebGLRenderingContext, width: number, height: number) { + this.width = width; + this.height = height; + + for (let ii = 0; ii < 2; ++ii) { + const texture = createAndSetupTexture(gl); + this.textures.push(texture); + + // make the texture the same size as the image + gl.texImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); + + // Create a framebuffer + const fbo = gl.createFramebuffer(); + this.framebuffers.push(fbo); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + // Attach a texture to it. + gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + } + } + + render(gl: WebGLRenderingContext) { + this.stages.forEach( (item, i) => { + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i%2]); + item.render(gl); + gl.bindTexture(gl.TEXTURE_2D, this.textures[i % 2]); + }); + } +} + +class Stage implements Renderable { + program: Shader; + uniforms: Dictionary; + + render(gl: WebGLRenderingContext) { + this.program.bind(gl); + + for (let name in this.uniforms) { + this.program.uniform(gl, name, this.uniforms[name]); + } + } +} diff --git a/frontend/www/webgl/renderer.ts b/frontend/www/webgl/renderer.ts new file mode 100644 index 0000000..2c4f6fa --- /dev/null +++ b/frontend/www/webgl/renderer.ts @@ -0,0 +1,72 @@ + +import { IndexBuffer } from './buffer'; +import { Shader, Uniform1i } from './shader'; +import { VertexArray } from './vertexBufferLayout'; +import { Texture } from './texture'; + +export interface Renderable { + render(gl: WebGLRenderingContext): void; +} + +export class Renderer { + renderables: Renderable[]; + + indexBuffers: IndexBuffer[]; + vertexArrays: VertexArray[]; + shaders: Shader[]; + textures: Texture[]; + + constructor() { + this.indexBuffers = []; + this.vertexArrays = []; + this.shaders = []; + this.textures = []; + } + + addRenderable(item: Renderable) { + this.renderables.push(item); + } + + addToDraw(indexBuffer: IndexBuffer, vertexArray: VertexArray, shader: Shader, texture?: Texture): number { + this.indexBuffers.push(indexBuffer); + this.vertexArrays.push(vertexArray); + this.shaders.push(shader); + this.textures.push(texture); + + return this.indexBuffers.length - 1; + } + + render(gl: WebGLRenderingContext) { + const maxTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); + let texLocation = 0; + + for(let i = 0; i < this.indexBuffers.length; i ++) { + const indexBuffer = this.indexBuffers[i]; + const vertexArray = this.vertexArrays[i]; + const shader = this.shaders[i]; + const texture = this.textures[i]; + + if (texture) { + + shader.uniform(gl, texture.name, new Uniform1i(texLocation)); + texture.bind(gl, texLocation); + + texLocation ++; + if (texLocation > maxTextures) { + console.error("Using too many textures, this is not supported yet\nUndefined behaviour!"); + } + } + + if (vertexArray && shader) { + vertexArray.bind(gl, shader); + + if (indexBuffer) { + indexBuffer.bind(gl); + gl.drawElements(gl.TRIANGLES, indexBuffer.getCount(), gl.UNSIGNED_SHORT, 0); + } else { + console.error("IndexBuffer is required to render, for now"); + } + } + } + } +} diff --git a/frontend/www/webgl/shader.ts b/frontend/www/webgl/shader.ts new file mode 100644 index 0000000..409d81a --- /dev/null +++ b/frontend/www/webgl/shader.ts @@ -0,0 +1,305 @@ +import { Dictionary } from './util'; + +function error(msg: string) { + console.log(msg); +} + +const defaultShaderType = [ + "VERTEX_SHADER", + "FRAGMENT_SHADER" +]; + +function loadShader( + gl: WebGLRenderingContext, + shaderSource: string, + shaderType: number, + opt_errorCallback: any, +): WebGLShader { + var errFn = opt_errorCallback || error; + // Create the shader object + var shader = gl.createShader(shaderType); + + // Load the shader source + gl.shaderSource(shader, shaderSource); + + // Compile the shader + gl.compileShader(shader); + + // Check the compile status + var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + // Something went wrong during compilation; get the error + var lastError = gl.getShaderInfoLog(shader); + errFn("*** Error compiling shader '" + shader + "':" + lastError); + gl.deleteShader(shader); + return null; + } + + console.log("created shader with source"); + console.log(shaderSource); + + return shader; +} + +function createProgram( + gl: WebGLRenderingContext, + shaders: WebGLShader[], + opt_attribs: string[], + opt_locations: number[], + opt_errorCallback: any, +): WebGLProgram { + var errFn = opt_errorCallback || error; + var program = gl.createProgram(); + shaders.forEach(function (shader) { + gl.attachShader(program, shader); + }); + if (opt_attribs) { + opt_attribs.forEach(function (attrib, ndx) { + gl.bindAttribLocation( + program, + opt_locations ? opt_locations[ndx] : ndx, + attrib); + }); + } + gl.linkProgram(program); + + // Check the link status + var linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + // something went wrong with the link + var lastError = gl.getProgramInfoLog(program); + errFn("Error in program linking:" + lastError); + + gl.deleteProgram(program); + return null; + } + return program; +} + +function createShaderFromScript( + gl: WebGLRenderingContext, + scriptId: string, + context: Dictionary, + opt_shaderType: number, + opt_errorCallback: any, +): WebGLShader { + var shaderSource = ""; + var shaderType; + var shaderScript = document.getElementById(scriptId) as HTMLScriptElement; + if (!shaderScript) { + console.log("*** Error: unknown script element" + scriptId); + } + shaderSource = shaderScript.text; + + for (let key in context) { + console.log("substitute " + key); + shaderSource = shaderSource.replace(new RegExp("\\$" + key, 'g'), context[key]); + } + + if (!opt_shaderType) { + if (shaderScript.type === "x-shader/x-vertex") { + shaderType = 35633; + } else if (shaderScript.type === "x-shader/x-fragment") { + shaderType = 35632; + } else if (shaderType !== gl.VERTEX_SHADER && shaderType !== gl.FRAGMENT_SHADER) { + console.log("*** Error: unknown shader type"); + } + } + + return loadShader( + gl, shaderSource, opt_shaderType ? opt_shaderType : shaderType, + opt_errorCallback); +} + +export class Shader { + shader: WebGLProgram; + uniformCache: Dictionary; + attribCache: Dictionary; + + static createProgramFromScripts( + gl: WebGLRenderingContext, + shaderScriptIds: string[], + context = {}, + opt_attribs?: string[], + opt_locations?: number[], + opt_errorCallback?: any, + ): Shader { + var shaders = []; + for (var ii = 0; ii < shaderScriptIds.length; ++ii) { + shaders.push(createShaderFromScript( + gl, shaderScriptIds[ii], context, (gl as any)[defaultShaderType[ii % 2]] as number, opt_errorCallback)); + } + return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); + } + + static async createProgramFromUrls( + gl: WebGLRenderingContext, + vert_url: string, + frag_url: string, + context?: Dictionary, + opt_attribs?: string[], + opt_locations?: number[], + opt_errorCallback?: any, + ): Promise { + const sources = (await Promise.all([ + fetch(vert_url).then((r) => r.text()), + fetch(frag_url).then((r) => r.text()), + ])).map(x => { + for (let key in context) { + x = x.replace(new RegExp("\\$" + key, 'g'), context[key]); + } + return x; + }); + + const shaders = [ + loadShader(gl, sources[0], 35633, opt_errorCallback), + loadShader(gl, sources[1], 35632, opt_errorCallback), + ]; + return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); + } + + constructor(shader: WebGLProgram) { + this.shader = shader; + this.uniformCache = {}; + this.attribCache = {}; + } + + bind(gl: WebGLRenderingContext) { + gl.useProgram(this.shader); + } + + // Different locations have different types :/ + getUniformLocation(gl: WebGLRenderingContext, name: string): WebGLUniformLocation { + if (this.uniformCache[name] === undefined) { + this.uniformCache[name] = gl.getUniformLocation(this.shader, name); + } + + return this.uniformCache[name]; + } + + getAttribLocation(gl: WebGLRenderingContext, name: string): number { + if (this.attribCache[name] === undefined) { + this.attribCache[name] = gl.getAttribLocation(this.shader, name); + } + + return this.attribCache[name]; + } + + uniform( + gl: WebGLRenderingContext, + name: string, + uniform: T, + ) { + this.bind(gl); + const location = this.getUniformLocation(gl, name); + if (location < 0) { + console.log("No location found with name " + name); + } + + uniform.setUniform(gl, location); + } +} + +export interface Uniform { + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation): void; +} + +export class Uniform2fv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform2fv(location, this.data); + } +} + +export class Uniform3fv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform3fv(location, this.data); + } +} + +export class Uniform1iv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1iv(location, this.data); + } +} + +export class Uniform1i implements Uniform { + texture: number; + + constructor(texture: number) { + this.texture = texture; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1i(location, this.texture); + } +} + +export class Uniform1f implements Uniform { + texture: number; + + constructor(texture: number) { + this.texture = texture; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1f(location, this.texture); + } +} + +export class Uniform2f implements Uniform { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform2f(location, this.x, this.y); + } +} + +export class Uniform4f implements Uniform { + v0: number; + v1: number; + v2: number; + v3: number; + + constructor(vec: number[]) { + this.v0 = vec[0]; + this.v1 = vec[1]; + this.v2 = vec[2]; + this.v3 = vec[3]; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform4f(location, this.v0, this.v1, this.v2, this.v3); + } +} + +export class UniformMatrix3fv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniformMatrix3fv(location, false, this.data); + } +} diff --git a/frontend/www/webgl/texture.ts b/frontend/www/webgl/texture.ts new file mode 100644 index 0000000..a9cbb39 --- /dev/null +++ b/frontend/www/webgl/texture.ts @@ -0,0 +1,68 @@ + + +// TODO: fix texture locations, not use only 0 +export class Texture { + texture: WebGLTexture; + image: HTMLImageElement; + loaded: boolean; + name: string; + + constructor( + gl: WebGLRenderingContext, + path: string, + name: string, + ) { + this.loaded = false; + this.name = name; + + this.image = new Image(); + this.image.onload = () => this.handleImageLoaded(gl); + this.image.onerror = error; + this.image.src = path; + + this.texture = gl.createTexture(); + this.bind(gl); + + 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); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, + gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + } + + handleImageLoaded(gl: WebGLRenderingContext) { + console.log('handling image loaded'); + this.bind(gl); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image); + + this.unbind(gl); + + this.loaded = true; + } + + bind(gl: WebGLRenderingContext, location=0) { + gl.activeTexture(gl.TEXTURE0 + location); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + } + + unbind(gl: WebGLRenderingContext) { + gl.bindTexture(gl.TEXTURE_2D, null); + } + + + getWidth(): number { + return this.image.width; + } + + getHeight(): number { + return this.image.height; + } +} + +function error(e: any) { + console.error("IMAGE LOAD ERROR"); + console.error(e); +} diff --git a/frontend/www/webgl/util.ts b/frontend/www/webgl/util.ts new file mode 100644 index 0000000..7aa4a6c --- /dev/null +++ b/frontend/www/webgl/util.ts @@ -0,0 +1,129 @@ + +export interface Dictionary { + [Key: string]: T; +} + + +interface OnLoadable { + onload: any; +} + +export function onload2promise(obj: T): Promise { + return new Promise(resolve => { + obj.onload = () => resolve(obj); + }); +} + +export function resizeCanvasToDisplaySize( + canvas: HTMLCanvasElement, + multiplier?: number, +): boolean { + multiplier = multiplier || 1; + var width = canvas.clientWidth * multiplier | 0; + var height = canvas.clientHeight * multiplier | 0; + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; +} + +export class FPSCounter { + last: number; + count: number; + constructor() { + this.last = 0; + this.count = 0; + } + + frame(now: number) { + this.count += 1; + if (now - this.last > 1) { + this.last = now; + console.log(this.count + " fps"); + this.count = 0; + } + } +} + +export class M3 { + _data: any; + + constructor(data: any) { + this._data = data; + } + + static ident(): M3 { + return new M3([ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ]); + } + + multiply(other: M3): M3 { + const a = this._data; + const b = other._data; + + var a00 = a[0 * 3 + 0]; + var a01 = a[0 * 3 + 1]; + var a02 = a[0 * 3 + 2]; + var a10 = a[1 * 3 + 0]; + var a11 = a[1 * 3 + 1]; + var a12 = a[1 * 3 + 2]; + var a20 = a[2 * 3 + 0]; + var a21 = a[2 * 3 + 1]; + var a22 = a[2 * 3 + 2]; + var b00 = b[0 * 3 + 0]; + var b01 = b[0 * 3 + 1]; + var b02 = b[0 * 3 + 2]; + var b10 = b[1 * 3 + 0]; + var b11 = b[1 * 3 + 1]; + var b12 = b[1 * 3 + 2]; + var b20 = b[2 * 3 + 0]; + var b21 = b[2 * 3 + 1]; + var b22 = b[2 * 3 + 2]; + + return new M3([ + b00 * a00 + b01 * a10 + b02 * a20, + b00 * a01 + b01 * a11 + b02 * a21, + b00 * a02 + b01 * a12 + b02 * a22, + b10 * a00 + b11 * a10 + b12 * a20, + b10 * a01 + b11 * a11 + b12 * a21, + b10 * a02 + b11 * a12 + b12 * a22, + b20 * a00 + b21 * a10 + b22 * a20, + b20 * a01 + b21 * a11 + b22 * a21, + b20 * a02 + b21 * a12 + b22 * a22, + ]); + } + + translation(x: number, y: number): M3 { + const out = [...this._data]; + out[6] += x; + out[7] += y; + return new M3(out); + } + + rotate(rad: number): M3 { + var c = Math.cos(rad); + var s = Math.sin(rad); + + const out = new M3([...this._data]); + + return out.multiply(new M3([ + c, -s, 0, + s, c, 0, + 0, 0, 1 + ])); + } + + scale(s_x: number, s_y = s_x, s_z = 1): M3 { + const out = new M3([...this._data]); + return out.multiply(new M3([ + s_x, 0, 0, + 0, s_y, 0, + 0, 0, s_z, + ])); + } +} diff --git a/frontend/www/webgl/vertexArray.ts b/frontend/www/webgl/vertexArray.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/www/webgl/vertexBufferLayout.ts b/frontend/www/webgl/vertexBufferLayout.ts new file mode 100644 index 0000000..22fd868 --- /dev/null +++ b/frontend/www/webgl/vertexBufferLayout.ts @@ -0,0 +1,112 @@ +import { Buffer, VertexBuffer } from './buffer'; +import { Shader } from './shader'; + +export class VertexBufferElement { + type: number; + amount: number; + type_size: number; + normalized: boolean; + index: string; + + constructor( + type: number, + amount: number, + type_size: number, + index: string, + normalized: boolean, + ) { + this.type = type; + this.amount = amount; + this.type_size = type_size; + this.normalized = normalized; + this.index = index; + } +} + +export class VertexBufferLayout { + elements: VertexBufferElement[]; + stride: number; + offset: number; + + constructor(offset = 0) { + this.elements = []; + this.stride = 0; + this.offset = offset; + } + + // Maybe wrong normalized type + push( + type: number, + amount: number, + type_size: number, + index: string, + normalized = false, + ) { + this.elements.push(new VertexBufferElement(type, amount, type_size, index, normalized)); + this.stride += amount * type_size; + } + + getElements(): VertexBufferElement[] { + return this.elements; + } + + getStride(): number { + return this.stride; + } +} + +// glEnableVertexAttribArray is to specify what location of the current program the follow data is needed +// glVertexAttribPointer tells gl that that data is at which location in the supplied data +export class VertexArray { + // There is no renderer ID, always at bind buffers and use glVertexAttribPointer + buffers: Buffer[]; + layouts: VertexBufferLayout[]; + + constructor() { + this.buffers = []; + this.layouts = []; + } + + addBuffer(vb: VertexBuffer, layout: VertexBufferLayout) { + this.buffers.push(vb); + this.layouts.push(layout); + } + + /// Bind buffers providing program data + bind(gl: WebGLRenderingContext, shader: Shader) { + shader.bind(gl); + for(let i = 0; i < this.buffers.length; i ++) { + const buffer = this.buffers[i]; + const layout = this.layouts[i]; + + buffer.bind(gl); + const elements = layout.getElements(); + let offset = layout.offset; + + for (let j = 0; j < elements.length; j ++) { + const element = elements[j]; + const location = shader.getAttribLocation(gl, element.index); + + if (location >= 0) { + gl.enableVertexAttribArray(location); + gl.vertexAttribPointer( + location, element.amount, element.type, + element.normalized, layout.stride, offset + ); + } + + offset += element.amount * element.type_size; + } + } + } + + /// Undo bind operation + unbind(gl: WebGLRenderingContext) { + this.layouts.forEach((layout) => { + layout.getElements().forEach((_, index) => { + gl.disableVertexAttribArray(index); + }); + }) + } +} + diff --git a/frontend/www/webpack.config.js b/frontend/www/webpack.config.js index 80ad814..1b0d636 100644 --- a/frontend/www/webpack.config.js +++ b/frontend/www/webpack.config.js @@ -2,12 +2,24 @@ const CopyWebpackPlugin = require("copy-webpack-plugin"); const path = require('path'); module.exports = { - entry: "./bootstrap.js", - output: { - path: path.resolve(__dirname, "dist"), - filename: "bootstrap.js", + mode: 'development', + entry: './bootstrap.js', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js', '.wasm' ] + }, + output: { + filename: 'bootstrap.js', + path: path.resolve(__dirname, 'dist') }, - mode: "development", plugins: [ new CopyWebpackPlugin(['index.html']) ],