This commit is contained in:
ajuvercr 2019-09-16 21:18:01 +02:00
parent 2ad7354de5
commit c846042597
17 changed files with 1239 additions and 16 deletions

View file

@ -43,7 +43,7 @@ pub fn run(args : Vec<String>) {
let ids: HashMap<util::Identifier, util::PlayerId> = (0..number_of_clients).map(|x| (rand::thread_rng().gen::<u64>().into(), x.into())).collect(); let ids: HashMap<util::Identifier, util::PlayerId> = (0..number_of_clients).map(|x| (rand::thread_rng().gen::<u64>().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)); let game = planetwars::PlanetWarsGame::new(config.create_game(number_of_clients as usize));
println!("Tokens:"); println!("Tokens:");

View file

@ -5,6 +5,8 @@ use serde_json;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::TryInto; use std::convert::TryInto;
use std::fs::File;
use std::io::Write;
mod pw_config; mod pw_config;
mod pw_serializer; mod pw_serializer;
@ -17,25 +19,30 @@ pub use pw_config::Config;
pub struct PlanetWarsGame { pub struct PlanetWarsGame {
state: pw_rules::PlanetWars, state: pw_rules::PlanetWars,
planet_map: HashMap<String, usize>, planet_map: HashMap<String, usize>,
log_file: File
} }
impl PlanetWarsGame { impl PlanetWarsGame {
pub fn new(state: pw_rules::PlanetWars) -> Self { pub fn new(state: pw_rules::PlanetWars) -> Self {
let planet_map = state.planets.iter().map(|p| (p.name.clone(), p.id)).collect(); let planet_map = state.planets.iter().map(|p| (p.name.clone(), p.id)).collect();
let file = File::create("game.json").unwrap();
Self { Self {
state, planet_map state, planet_map,
log_file: file,
} }
} }
fn dispatch_state(&self, were_alive: Vec<usize>, updates: &mut Vec<game::Update>, ) { fn dispatch_state(&mut self, were_alive: Vec<usize>, updates: &mut Vec<game::Update>, ) {
let state = pw_serializer::serialize(&self.state); 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)) { 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 = 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) proto::ServerMessage::GameState(state)
} else { } else {
proto::ServerMessage::FinalState(state) proto::ServerMessage::FinalState(state)
@ -45,7 +52,7 @@ impl PlanetWarsGame {
game::Update::Player((player.id as u64).into(), serde_json::to_vec(&state).unwrap()) 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())); updates.push(game::Update::Kick((player.id as u64).into()));
} }
} }

View file

@ -7,3 +7,11 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [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 }

View file

@ -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() { fn main() {
println!("Hello, world!"); let args: Vec<String> = 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<ReactorId>,
broker: BrokerHandle,
id: u64,
args: Vec<String>,
}
impl ClientReactor {
fn params<C: Ctx>(self) -> CoreParams<Self, C> {
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<C: Ctx>(
&mut self,
handle: &mut ReactorHandle<C>,
_: 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<C: Ctx>(
&mut self,
handle: &mut ReactorHandle<C>,
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::<bot_return::Owned>::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::<identify::Owned>::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<C: Ctx>(foreign_id: ReactorId) -> LinkParams<Self, C> {
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<C: Ctx>(
&mut self,
handle: &mut LinkHandle<C>,
id: identify::Reader,
) -> Result<()> {
let id = id.get_key();
let mut chat_message = MsgBuffer::<identify::Owned>::new();
chat_message.build(|b| {
b.set_key(id);
});
handle.send_message(chat_message).display();
Ok(())
}
fn close_handler<C: Ctx>(
&mut self,
handle: &mut LinkHandle<C>,
_: terminate_stream::Reader,
) -> Result<()>
{
// also close our end of the stream
handle.close_link()?;
return Ok(());
}
}
struct HostLink;
impl HostLink {
fn params<C: Ctx>(remote_id: ReactorId) -> LinkParams<Self, C> {
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<C: Ctx>(
&mut self,
handle: &mut LinkHandle<C>,
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::<client_message::Owned>::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<C: Ctx>(
&mut self,
handle: &mut LinkHandle<C>,
_: 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<C: Ctx>(
&mut self,
handle: &mut LinkHandle<C>,
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::<bot_input::Owned>::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<C: Ctx>(foreign_id: ReactorId) -> LinkParams<Self, C> {
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<C: Ctx>(foreign_id: ReactorId) -> LinkParams<Self, C> {
let mut params = LinkParams::new(foreign_id, Self);
params.external_handler(
actor_joined::Owned,
CtxHandler::new(actor_joined::e_to_i),
);
return params;
}
} }

View file

@ -1,5 +1,6 @@
import { Game } from "planetwars"; import { Game } from "planetwars";
import { memory } from "planetwars/plantwars_bg" import { memory } from "planetwars/plantwars_bg"
import { Shader } from "./webgl/shader"
const URL = window.location.origin+window.location.pathname; const URL = window.location.origin+window.location.pathname;
const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1);

View file

@ -4063,6 +4063,12 @@
"sha.js": "^2.4.8" "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": { "pify": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@ -4094,8 +4100,7 @@
} }
}, },
"planetwars": { "planetwars": {
"version": "file:../pkg", "version": "file:../pkg"
"dev": true
}, },
"portfinder": { "portfinder": {
"version": "1.0.21", "version": "1.0.21",
@ -5192,6 +5197,70 @@
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"dev": true "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": { "tslib": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
@ -5220,6 +5289,12 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true "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": { "union-value": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",

View file

@ -2,7 +2,8 @@
"name": "create-wasm-app", "name": "create-wasm-app",
"version": "0.1.0", "version": "0.1.0",
"description": "create an app to consume rust-generated wasm packages", "description": "create an app to consume rust-generated wasm packages",
"main": "index.js", "main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": { "scripts": {
"build": "webpack --config webpack.config.js", "build": "webpack --config webpack.config.js",
"start": "webpack-dev-server" "start": "webpack-dev-server"
@ -18,6 +19,9 @@
"webpack", "webpack",
"mozaic" "mozaic"
], ],
"dependencies": {
"planetwars": "file:../pkg"
},
"author": "Arthur Vercruysse <arthur.vercruysse@outlook.com>", "author": "Arthur Vercruysse <arthur.vercruysse@outlook.com>",
"license": "(MIT OR Apache-2.0)", "license": "(MIT OR Apache-2.0)",
"bugs": { "bugs": {
@ -25,8 +29,9 @@
}, },
"homepage": "https://github.com/ajuvercr/Planetwars#Readme", "homepage": "https://github.com/ajuvercr/Planetwars#Readme",
"devDependencies": { "devDependencies": {
"planetwars": "file:../pkg",
"webpack": "^4.29.3", "webpack": "^4.29.3",
"ts-loader": "^6.0.2",
"typescript": "^3.5.2",
"webpack-cli": "^3.1.0", "webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5", "webpack-dev-server": "^3.1.5",
"copy-webpack-plugin": "^5.0.0" "copy-webpack-plugin": "^5.0.0"

View file

@ -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"
]
}

View file

@ -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);
}
}

View file

@ -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<Uniform>;
render(gl: WebGLRenderingContext) {
this.program.bind(gl);
for (let name in this.uniforms) {
this.program.uniform(gl, name, this.uniforms[name]);
}
}
}

View file

@ -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");
}
}
}
}
}

View file

@ -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<any>,
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<WebGLUniformLocation>;
attribCache: Dictionary<number>;
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<string>,
opt_attribs?: string[],
opt_locations?: number[],
opt_errorCallback?: any,
): Promise<Shader> {
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<T extends 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);
}
}

View file

@ -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);
}

129
frontend/www/webgl/util.ts Normal file
View file

@ -0,0 +1,129 @@
export interface Dictionary<T> {
[Key: string]: T;
}
interface OnLoadable {
onload: any;
}
export function onload2promise<T extends OnLoadable>(obj: T): Promise<T> {
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,
]));
}
}

View file

View file

@ -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);
});
})
}
}

View file

@ -2,12 +2,24 @@ const CopyWebpackPlugin = require("copy-webpack-plugin");
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
entry: "./bootstrap.js", mode: 'development',
output: { entry: './bootstrap.js',
path: path.resolve(__dirname, "dist"), module: {
filename: "bootstrap.js", 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: [ plugins: [
new CopyWebpackPlugin(['index.html']) new CopyWebpackPlugin(['index.html'])
], ],