From 02758a69ac49cc437ed27628b64e08fd443758b8 Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Tue, 12 Sep 2017 09:20:19 +0200 Subject: Implement simple map renderer --- dist/resources/map/test.json | 6 ++-- package-lock.json | 9 +++-- package.json | 4 ++- src/index.ts | 23 +++++++++---- src/model/MapData.ts | 26 ++++++++++++++ src/util.ts | 33 ++++++++++++++++++ src/view/MapLoader.ts | 59 ++++++++++++++++++++++++++++++++ src/view/MapView.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++ src/view/Renderer.ts | 29 ++++++++-------- src/view/Scene.ts | 41 ---------------------- src/view/default.fs | 7 +++- src/view/default.vs | 13 ++++--- tsconfig.json | 2 +- 13 files changed, 259 insertions(+), 74 deletions(-) create mode 100644 src/model/MapData.ts create mode 100644 src/util.ts create mode 100644 src/view/MapLoader.ts create mode 100644 src/view/MapView.ts delete mode 100644 src/view/Scene.ts diff --git a/dist/resources/map/test.json b/dist/resources/map/test.json index 4689076..2cf324f 100644 --- a/dist/resources/map/test.json +++ b/dist/resources/map/test.json @@ -1,8 +1,8 @@ { "tiles": { - "G": {"file": "grass"}, - "<": {"file": "road", "subtile": 0}, - ">": {"file": "road", "subtile": 1} + "G": "grass", + "<": "road_left", + ">": "road_right" }, "collision": [ "00000000000110000000000000000000", diff --git a/package-lock.json b/package-lock.json index 359b6a5..23eeb96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4ddc777..0bb144b 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/index.ts b/src/index.ts index 718a321..6390017 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,24 @@ -import Renderer from './view/Renderer'; -import Scene from './view/Scene'; +import MapData from './model/MapData'; +import Renderer from './view/Renderer'; +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(); +}; diff --git a/src/model/MapData.ts b/src/model/MapData.ts new file mode 100644 index 0000000..b83e146 --- /dev/null +++ b/src/model/MapData.ts @@ -0,0 +1,26 @@ +import {mapFromObject} from '../util'; + + +interface Input { + tiles: {[key: string]: string}; + collision: string[]; + layers: string[][][]; +} + +export default class MapData { + tiles: Map; + 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; + } +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..337bf05 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,33 @@ +import * as _ from 'lodash'; + + +export function mapFromObject(obj: {[key: string]: T}): Map { + return new Map(_.toPairs(obj)); +} + +export function mapValues(f: (v: V1) => V2, map: Map): Map { + let ret: Map = new Map(); + + for (let [k, v] of map) + ret.set(k, f(v)); + + return ret; +} + +export async function mapValuesAsync(f: (v: V1) => Promise, map: Map): Promise> { + let ret: Map = 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; +} diff --git a/src/view/MapLoader.ts b/src/view/MapLoader.ts new file mode 100644 index 0000000..eca8b75 --- /dev/null +++ b/src/view/MapLoader.ts @@ -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 { + 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): Promise> { + return mapValuesAsync(loadImage, urls); +} + +function loadTiles(tiles: Map): Promise> { + return loadImages(mapValues(t => `resources/sprite/tile/${t}.png`, tiles)); +} + +function mkTileTexture(gl: WebGLRenderingContext, tiles: Map): [WebGLTexture, Map] { + let canvas = document.createElement('canvas'); + canvas.width = nextPowerOf2(tiles.size) * MapView.tileSize; + canvas.height = MapView.tileSize; + + let i = 0; + let ret: Map = 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 { + let tiles = await loadTiles(mapData.tiles); + let [tileTexture, tileMap] = mkTileTexture(r.gl, tiles); + + return new MapView(r, mapData, tileTexture, tileMap); +} diff --git a/src/view/MapView.ts b/src/view/MapView.ts new file mode 100644 index 0000000..61b8336 --- /dev/null +++ b/src/view/MapView.ts @@ -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) { + 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; diff --git a/src/view/Renderer.ts b/src/view/Renderer.ts index 556cfe3..1b9dd84 100644 --- a/src/view/Renderer.ts +++ b/src/view/Renderer.ts @@ -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; diff --git a/src/view/Scene.ts b/src/view/Scene.ts deleted file mode 100644 index 0de7546..0000000 --- a/src/view/Scene.ts +++ /dev/null @@ -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; diff --git a/src/view/default.fs b/src/view/default.fs index 7a085b6..351fed7 100644 --- a/src/view/default.fs +++ b/src/view/default.fs @@ -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)); } diff --git a/src/view/default.vs b/src/view/default.vs index 7c4eaeb..4715a17 100644 --- a/src/view/default.vs +++ b/src/view/default.vs @@ -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; } diff --git a/tsconfig.json b/tsconfig.json index 06a3171..c0a6058 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "outDir": "./dist/", "sourceMap": true, "module": "commonjs", - "target": "es5", + "target": "ES2015", "lib": [ "es2015", "dom" ], "strict": true } -- cgit v1.2.3