Implement simple map renderer
This commit is contained in:
parent
a5e69edc5a
commit
02758a69ac
13 changed files with 259 additions and 74 deletions
6
dist/resources/map/test.json
vendored
6
dist/resources/map/test.json
vendored
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"tiles": {
|
||||
"G": {"file": "grass"},
|
||||
"<": {"file": "road", "subtile": 0},
|
||||
">": {"file": "road", "subtile": 1}
|
||||
"G": "grass",
|
||||
"<": "road_left",
|
||||
">": "road_right"
|
||||
},
|
||||
"collision": [
|
||||
"00000000000110000000000000000000",
|
||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -8,6 +8,12 @@
|
|||
"integrity": "sha512-8xmF+Zx+HsfSU4vABlqdjNSDZVKY8JLx8bjD5INcESmO7nXmcYatry1LPYPK/XMuYNxrGrrvkcXJdS1pgtRfQg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.74",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.74.tgz",
|
||||
"integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"accepts": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz",
|
||||
|
@ -1702,8 +1708,7 @@
|
|||
"lodash": {
|
||||
"version": "4.17.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
|
||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=",
|
||||
"dev": true
|
||||
"integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
|
||||
},
|
||||
"loglevel": {
|
||||
"version": "1.4.1",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/gl-matrix": "^2.3.0",
|
||||
"@types/lodash": "^4.14.74",
|
||||
"raw-loader": "^0.5.1",
|
||||
"ts-loader": "^2.3.6",
|
||||
"typescript": "^2.5.2",
|
||||
|
@ -14,6 +15,7 @@
|
|||
"webpack-dev-server": "^2.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"gl-matrix": "^2.4.0"
|
||||
"gl-matrix": "^2.4.0",
|
||||
"lodash": "^4.17.4"
|
||||
}
|
||||
}
|
||||
|
|
23
src/index.ts
23
src/index.ts
|
@ -1,13 +1,24 @@
|
|||
import MapData from './model/MapData';
|
||||
|
||||
import Renderer from './view/Renderer';
|
||||
import Scene from './view/Scene';
|
||||
import {loadMap} from './view/MapLoader';
|
||||
|
||||
|
||||
(() => {
|
||||
window.onload = () => {
|
||||
const canvas = document.getElementById('rpgedit') as HTMLCanvasElement;
|
||||
if (!canvas)
|
||||
return;
|
||||
|
||||
const renderer = new Renderer(canvas);
|
||||
const scene = new Scene(renderer);
|
||||
scene.draw();
|
||||
})();
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.addEventListener('load', async function() {
|
||||
let mapDef = new MapData(JSON.parse(this.responseText));
|
||||
|
||||
let mapView = await loadMap(renderer, mapDef);
|
||||
mapView.draw();
|
||||
});
|
||||
|
||||
xhr.open('GET', 'resources/map/test.json', true);
|
||||
xhr.send();
|
||||
};
|
||||
|
|
26
src/model/MapData.ts
Normal file
26
src/model/MapData.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import {mapFromObject} from '../util';
|
||||
|
||||
|
||||
interface Input {
|
||||
tiles: {[key: string]: string};
|
||||
collision: string[];
|
||||
layers: string[][][];
|
||||
}
|
||||
|
||||
export default class MapData {
|
||||
tiles: Map<string, string>;
|
||||
collision: string[];
|
||||
layers: string[][][];
|
||||
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
constructor(data: Input) {
|
||||
this.tiles = mapFromObject(data.tiles);
|
||||
this.collision = data.collision;
|
||||
this.layers = data.layers;
|
||||
|
||||
this.height = this.collision.length;
|
||||
this.width = this.collision[0].length;
|
||||
}
|
||||
}
|
33
src/util.ts
Normal file
33
src/util.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import * as _ from 'lodash';
|
||||
|
||||
|
||||
export function mapFromObject<T>(obj: {[key: string]: T}): Map<string, T> {
|
||||
return new Map(_.toPairs(obj));
|
||||
}
|
||||
|
||||
export function mapValues<K, V1, V2>(f: (v: V1) => V2, map: Map<K, V1>): Map<K, V2> {
|
||||
let ret: Map<K, V2> = new Map();
|
||||
|
||||
for (let [k, v] of map)
|
||||
ret.set(k, f(v));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function mapValuesAsync<K, V1, V2>(f: (v: V1) => Promise<V2>, map: Map<K, V1>): Promise<Map<K, V2>> {
|
||||
let ret: Map<K, V2> = new Map();
|
||||
|
||||
for (let [k, v] of mapValues(f, map))
|
||||
ret.set(k, await v);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function nextPowerOf2(n: number): number {
|
||||
let i = 1;
|
||||
|
||||
while (i < n)
|
||||
i *= 2;
|
||||
|
||||
return i;
|
||||
}
|
59
src/view/MapLoader.ts
Normal file
59
src/view/MapLoader.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
import {mapValues, mapValuesAsync, nextPowerOf2} from '../util';
|
||||
|
||||
import Renderer from './Renderer';
|
||||
import MapView from './MapView';
|
||||
import MapData from '../model/MapData';
|
||||
|
||||
|
||||
function loadImage(url: string): Promise<HTMLImageElement> {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let img = new Image();
|
||||
img.addEventListener('load', () => { resolve(img); });
|
||||
img.addEventListener('error', () => { reject(Error('failed to load ' + url)); });
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
function loadImages(urls: Map<string, string>): Promise<Map<string, HTMLImageElement>> {
|
||||
return mapValuesAsync(loadImage, urls);
|
||||
}
|
||||
|
||||
function loadTiles(tiles: Map<string, string>): Promise<Map<string, HTMLImageElement>> {
|
||||
return loadImages(mapValues(t => `resources/sprite/tile/${t}.png`, tiles));
|
||||
}
|
||||
|
||||
function mkTileTexture(gl: WebGLRenderingContext, tiles: Map<string, HTMLImageElement>): [WebGLTexture, Map<string, number>] {
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = nextPowerOf2(tiles.size) * MapView.tileSize;
|
||||
canvas.height = MapView.tileSize;
|
||||
|
||||
let i = 0;
|
||||
let ret: Map<string, number> = new Map();
|
||||
let ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
|
||||
for (let [k, tile] of tiles) {
|
||||
ctx.drawImage(tile, i * MapView.tileSize, 0);
|
||||
ret.set(k, i++);
|
||||
}
|
||||
|
||||
let texture = gl.createTexture();
|
||||
if (!texture)
|
||||
throw new Error('unable to create texture');
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
|
||||
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, ret];
|
||||
}
|
||||
|
||||
|
||||
export async function loadMap(r: Renderer, mapData: MapData): Promise<MapView> {
|
||||
let tiles = await loadTiles(mapData.tiles);
|
||||
let [tileTexture, tileMap] = mkTileTexture(r.gl, tiles);
|
||||
|
||||
return new MapView(r, mapData, tileTexture, tileMap);
|
||||
}
|
81
src/view/MapView.ts
Normal file
81
src/view/MapView.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import * as _ from 'lodash';
|
||||
|
||||
import {nextPowerOf2} from '../util';
|
||||
|
||||
import Renderer from './Renderer';
|
||||
import MapData from '../model/MapData';
|
||||
|
||||
|
||||
class MapView {
|
||||
private redrawPending: boolean = false;
|
||||
|
||||
private vertexBuffer: WebGLBuffer;
|
||||
private textureBuffer: WebGLBuffer;
|
||||
|
||||
|
||||
private addTile(vertexData: number[], textureData: number[], x: number, y: number, tile: string, tileCount: number) {
|
||||
let tileID = this.tileMap.get(tile);
|
||||
if (tileID === undefined)
|
||||
throw new Error('invalid tile specifier in map data');
|
||||
|
||||
vertexData.push(x); vertexData.push(y);
|
||||
vertexData.push(x+1); vertexData.push(y);
|
||||
vertexData.push(x); vertexData.push(y+1);
|
||||
|
||||
vertexData.push(x); vertexData.push(y+1);
|
||||
vertexData.push(x+1); vertexData.push(y);
|
||||
vertexData.push(x+1); vertexData.push(y+1);
|
||||
|
||||
textureData.push((tileID) / tileCount); textureData.push(0);
|
||||
textureData.push((tileID+1) / tileCount); textureData.push(0);
|
||||
textureData.push((tileID) / tileCount); textureData.push(1);
|
||||
|
||||
textureData.push((tileID) / tileCount); textureData.push(1);
|
||||
textureData.push((tileID+1) / tileCount); textureData.push(0);
|
||||
textureData.push((tileID+1) / tileCount); textureData.push(1);
|
||||
}
|
||||
|
||||
constructor(private r: Renderer, private map: MapData, private tileTexture: WebGLTexture, private tileMap: Map<string, number>) {
|
||||
let vertexData: number[] = [];
|
||||
let textureData: number[] = [];
|
||||
|
||||
let tileCount = nextPowerOf2(tileMap.size);
|
||||
|
||||
for (let x = 0; x < map.width; x++) {
|
||||
for (let y = 0; y < map.height; y++) {
|
||||
this.addTile(vertexData, textureData, x, y, map.layers[0][y][x], tileCount);
|
||||
}
|
||||
}
|
||||
|
||||
this.vertexBuffer = r.createBuffer();
|
||||
r.gl.bindBuffer(r.gl.ARRAY_BUFFER, this.vertexBuffer);
|
||||
r.gl.bufferData(r.gl.ARRAY_BUFFER, new Float32Array(vertexData), r.gl.STATIC_DRAW);
|
||||
|
||||
this.textureBuffer = r.createBuffer();
|
||||
r.gl.bindBuffer(r.gl.ARRAY_BUFFER, this.textureBuffer);
|
||||
r.gl.bufferData(r.gl.ARRAY_BUFFER, new Float32Array(textureData), r.gl.STATIC_DRAW);
|
||||
}
|
||||
|
||||
draw(): void {
|
||||
this.r.gl.clear(this.r.gl.COLOR_BUFFER_BIT);
|
||||
|
||||
this.r.gl.activeTexture(this.r.gl.TEXTURE0);
|
||||
this.r.gl.bindTexture(this.r.gl.TEXTURE_2D, this.tileTexture);
|
||||
this.r.gl.uniform1i(this.r.samplerLoc, 0);
|
||||
|
||||
this.r.gl.bindBuffer(this.r.gl.ARRAY_BUFFER, this.vertexBuffer);
|
||||
this.r.gl.vertexAttribPointer(this.r.vertexPosLoc, 2, this.r.gl.FLOAT, false, 0, 0);
|
||||
|
||||
this.r.gl.bindBuffer(this.r.gl.ARRAY_BUFFER, this.textureBuffer);
|
||||
this.r.gl.vertexAttribPointer(this.r.textureCoordLoc, 2, this.r.gl.FLOAT, false, 0, 0);
|
||||
|
||||
this.r.gl.drawArrays(this.r.gl.TRIANGLES, 0, 6 * this.map.width * this.map.height);
|
||||
}
|
||||
}
|
||||
|
||||
module MapView {
|
||||
export const tileSize = 32;
|
||||
}
|
||||
|
||||
|
||||
export default MapView;
|
|
@ -1,15 +1,16 @@
|
|||
import {mat4} from 'gl-matrix';
|
||||
|
||||
|
||||
class Renderer {
|
||||
export default class Renderer {
|
||||
public gl: WebGLRenderingContext;
|
||||
|
||||
public vertexPosLoc: number;
|
||||
public textureCoordLoc: number;
|
||||
private viewportLoc: WebGLUniformLocation;
|
||||
private translateLoc: WebGLUniformLocation;
|
||||
public samplerLoc: WebGLUniformLocation;
|
||||
|
||||
private viewport: mat4 = mat4.create();
|
||||
private translate: mat4 = mat4.create();
|
||||
|
||||
private mkContext(): WebGLRenderingContext {
|
||||
let gl = (this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')) as WebGLRenderingContext|null;
|
||||
|
@ -45,9 +46,8 @@ class Renderer {
|
|||
|
||||
compileShader(type: number, src: string): WebGLShader {
|
||||
let shader = this.gl.createShader(type);
|
||||
if (!shader) {
|
||||
if (!shader)
|
||||
throw new Error('Unable to create shader');
|
||||
}
|
||||
|
||||
this.gl.shaderSource(shader, src);
|
||||
this.gl.compileShader(shader);
|
||||
|
@ -63,9 +63,8 @@ class Renderer {
|
|||
|
||||
private initShaders(): void {
|
||||
let shaderProgram = this.gl.createProgram();
|
||||
if (!shaderProgram) {
|
||||
if (!shaderProgram)
|
||||
throw new Error('Unable to create shader program');
|
||||
}
|
||||
|
||||
let vertexShader = this.compileShader(this.gl.VERTEX_SHADER, require('./default.vs'));
|
||||
let fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, require('./default.fs'));
|
||||
|
@ -87,11 +86,15 @@ class Renderer {
|
|||
|
||||
this.gl.useProgram(shaderProgram);
|
||||
|
||||
this.vertexPosLoc = this.getAttribLocation(shaderProgram, 'vertexPos');
|
||||
this.vertexPosLoc = this.getAttribLocation(shaderProgram, 'aVertexPos');
|
||||
this.gl.enableVertexAttribArray(this.vertexPosLoc);
|
||||
|
||||
this.viewportLoc = this.getUniformLocation(shaderProgram, 'viewport');
|
||||
this.translateLoc = this.getUniformLocation(shaderProgram, 'translate');
|
||||
this.textureCoordLoc = this.getAttribLocation(shaderProgram, 'aTextureCoord');
|
||||
this.gl.enableVertexAttribArray(this.textureCoordLoc);
|
||||
|
||||
this.viewportLoc = this.getUniformLocation(shaderProgram, 'uViewport');
|
||||
this.translateLoc = this.getUniformLocation(shaderProgram, 'uTranslate');
|
||||
this.samplerLoc = this.getUniformLocation(shaderProgram, 'uSampler');
|
||||
}
|
||||
|
||||
private setSize(): void {
|
||||
|
@ -102,11 +105,10 @@ class Renderer {
|
|||
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
mat4.identity(this.viewport);
|
||||
mat4.scale(this.viewport, this.viewport, [64 / w, 64 / h, 1.0]);
|
||||
mat4.scale(this.viewport, this.viewport, [2 * 64 / w, -2 * 64 / h, 1.0]);
|
||||
this.gl.uniformMatrix4fv(this.viewportLoc, false, this.viewport);
|
||||
|
||||
mat4.identity(this.translate);
|
||||
this.gl.uniformMatrix4fv(this.translateLoc, false, this.translate);
|
||||
this.gl.uniform2f(this.translateLoc, -5.0, -5.0);
|
||||
}
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
|
@ -115,10 +117,7 @@ class Renderer {
|
|||
this.initShaders();
|
||||
|
||||
this.gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
||||
this.gl.enable(this.gl.DEPTH_TEST);
|
||||
|
||||
this.setSize();
|
||||
}
|
||||
}
|
||||
|
||||
export default Renderer;
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
import Renderer from './Renderer';
|
||||
|
||||
class Scene {
|
||||
private triangleVertexPositionBuffer: WebGLBuffer;
|
||||
private squareVertexPositionBuffer: WebGLBuffer;
|
||||
|
||||
constructor(private r: Renderer) {
|
||||
this.triangleVertexPositionBuffer = r.createBuffer();
|
||||
r.gl.bindBuffer(r.gl.ARRAY_BUFFER, this.triangleVertexPositionBuffer);
|
||||
const triangleVertices = [
|
||||
-1.5, 1.0,
|
||||
-2.5, -1.0,
|
||||
-0.5, -1.0,
|
||||
];
|
||||
r.gl.bufferData(r.gl.ARRAY_BUFFER, new Float32Array(triangleVertices), r.gl.STATIC_DRAW);
|
||||
|
||||
this.squareVertexPositionBuffer = r.createBuffer();
|
||||
r.gl.bindBuffer(r.gl.ARRAY_BUFFER, this.squareVertexPositionBuffer);
|
||||
const squareVertices = [
|
||||
2.5, 1.0,
|
||||
0.5, 1.0,
|
||||
2.5, -1.0,
|
||||
0.5, -1.0,
|
||||
];
|
||||
r.gl.bufferData(r.gl.ARRAY_BUFFER, new Float32Array(squareVertices), r.gl.STATIC_DRAW);
|
||||
}
|
||||
|
||||
draw(): void {
|
||||
this.r.gl.clear(this.r.gl.COLOR_BUFFER_BIT | this.r.gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
this.r.gl.bindBuffer(this.r.gl.ARRAY_BUFFER, this.triangleVertexPositionBuffer);
|
||||
this.r.gl.vertexAttribPointer(this.r.vertexPosLoc, 2, this.r.gl.FLOAT, false, 0, 0);
|
||||
this.r.gl.drawArrays(this.r.gl.TRIANGLES, 0, 3);
|
||||
|
||||
this.r.gl.bindBuffer(this.r.gl.ARRAY_BUFFER, this.squareVertexPositionBuffer);
|
||||
this.r.gl.vertexAttribPointer(this.r.vertexPosLoc, 2, this.r.gl.FLOAT, false, 0, 0);
|
||||
this.r.gl.drawArrays(this.r.gl.TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
export default Scene;
|
|
@ -1,3 +1,8 @@
|
|||
varying highp vec2 vTextureCoord;
|
||||
|
||||
uniform sampler2D uSampler;
|
||||
|
||||
|
||||
void main(void) {
|
||||
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
gl_FragColor = texture2D(uSampler, vec2(vTextureCoord.x, vTextureCoord.y));
|
||||
}
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
attribute vec2 vertexPos;
|
||||
attribute vec2 aVertexPos;
|
||||
attribute vec2 aTextureCoord;
|
||||
|
||||
uniform mat4 uViewport;
|
||||
uniform vec2 uTranslate;
|
||||
|
||||
varying highp vec2 vTextureCoord;
|
||||
|
||||
uniform mat4 viewport;
|
||||
uniform mat4 translate;
|
||||
|
||||
void main(void) {
|
||||
gl_Position = viewport * translate * vec4(vertexPos, 0.0, 1.0);
|
||||
gl_Position = uViewport * vec4(aVertexPos + uTranslate, 0.0, 1.0);
|
||||
vTextureCoord = aTextureCoord;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"outDir": "./dist/",
|
||||
"sourceMap": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "ES2015",
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"strict": true
|
||||
}
|
||||
|
|
Reference in a new issue