@ -0,0 +1,32 @@ | |||||
const CANVAS_ID = "canvas"; | |||||
const GAT_FILE_ID = "gat_file"; | |||||
const canvas = document.getElementById(CANVAS_ID); | |||||
if (canvas === null) { | |||||
throw new Error("couldn't find element #" + CANVAS_ID); | |||||
} | |||||
const gl = canvas.getContext("webgl"); | |||||
if (gl === null) { | |||||
throw new Error("couldn't initialize webgl context"); | |||||
} | |||||
gl.clearColor(0, 0, 0, 1); | |||||
gl.clear(gl.COLOR_BUFFER_BIT); | |||||
if (typeof window.rojs !== "object") { | |||||
throw new Error("ro.js not found, is it loaded?"); | |||||
} | |||||
const gatFile = document.getElementById(GAT_FILE_ID); | |||||
gatFile.addEventListener("change", (e) => { | |||||
const file = gatFile.files[0]; | |||||
const reader = new FileReader(); | |||||
reader.onload = (e) => { | |||||
const gat = window.rojs.parseGAT(e.target.result); | |||||
window.rojs.renderGAT(gl, gat); | |||||
}; | |||||
reader.readAsArrayBuffer(file); | |||||
}); |
@ -0,0 +1,23 @@ | |||||
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
<title>ro.js example</title> | |||||
<style> | |||||
body { | |||||
margin-top: 1rem; | |||||
margin-left: 1rem; | |||||
} | |||||
.file-input { | |||||
margin-bottom: 1.5rem; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div class="file-input"> | |||||
<input id="gat_file" type="file" /> | |||||
</div> | |||||
<canvas id="canvas" width="500" height="500"></canvas> | |||||
<script src="/build/bundle.js"></script> | |||||
<script src="/example.js"></script> | |||||
</body> | |||||
</html> |
@ -0,0 +1,100 @@ | |||||
import DataScanner from "../util/scanner"; | |||||
export enum TileType { | |||||
WALKABLE, | |||||
OBSTRUCTED, | |||||
CLIFF, | |||||
} | |||||
export enum Corner { | |||||
TOP_LEFT, | |||||
TOP_RIGHT, | |||||
BOTTOM_RIGHT, | |||||
BOTTOM_LEFT, | |||||
} | |||||
export type TileAltitudes = Record<Corner, number>; | |||||
export type Tile = { | |||||
altitude: TileAltitudes; | |||||
ty: TileType; | |||||
}; | |||||
export type GAT = { | |||||
height: number; | |||||
tiles: Tile[]; | |||||
width: number; | |||||
}; | |||||
export class ParseError extends Error { | |||||
constructor(message: string) { | |||||
super(message); | |||||
} | |||||
} | |||||
const HEADER_MAGIC = 0x54415247; | |||||
const HEADER_SIZE_BYTES = 14; | |||||
const SUPPORTED_VERSION = "1.2"; | |||||
const TILE_SIZE_BYTES = 20; | |||||
const SCALING_FACTOR = -1 / 5; | |||||
const TERRAIN_TYPE_TO_TILE_TYPE: Record<number, TileType> = { | |||||
0: TileType.WALKABLE, | |||||
1: TileType.OBSTRUCTED, | |||||
5: TileType.CLIFF, | |||||
}; | |||||
export const parseGAT = (data: ArrayBuffer): GAT => { | |||||
const scanner = new DataScanner(data); | |||||
const magic = scanner.uint32(); | |||||
if (magic !== HEADER_MAGIC) { | |||||
throw new ParseError("invalid magic number"); | |||||
} | |||||
const version = `${scanner.uint8()}.${scanner.uint8()}`; | |||||
if (version !== SUPPORTED_VERSION) { | |||||
throw new ParseError(`unsupported file version ${version}`); | |||||
} | |||||
const width = scanner.uint32(); | |||||
const height = scanner.uint32(); | |||||
const expectedSize = HEADER_SIZE_BYTES + width * height * TILE_SIZE_BYTES; | |||||
if (data.byteLength !== expectedSize) { | |||||
throw new ParseError(`unexpected size of data`); | |||||
} | |||||
const tiles = []; | |||||
for (let i = 0; i < width * height; ++i) { | |||||
tiles[i] = parseTile(scanner); | |||||
} | |||||
return { | |||||
height, | |||||
tiles, | |||||
width, | |||||
}; | |||||
}; | |||||
const parseTile = (scanner: DataScanner): Tile => { | |||||
const bottomLeft = scanner.float32(); | |||||
const bottomRight = scanner.float32(); | |||||
const topLeft = scanner.float32(); | |||||
const topRight = scanner.float32(); | |||||
const terrainType = scanner.uint32() & 0xffff; | |||||
if (typeof TERRAIN_TYPE_TO_TILE_TYPE[terrainType] === "undefined") { | |||||
throw new Error(`unknown terrain type 0x${terrainType.toString(16)}`); | |||||
} | |||||
return { | |||||
ty: TERRAIN_TYPE_TO_TILE_TYPE[terrainType], | |||||
altitude: { | |||||
[Corner.TOP_LEFT]: topLeft * SCALING_FACTOR, | |||||
[Corner.TOP_RIGHT]: topRight * SCALING_FACTOR, | |||||
[Corner.BOTTOM_RIGHT]: bottomRight * SCALING_FACTOR, | |||||
[Corner.BOTTOM_LEFT]: bottomLeft * SCALING_FACTOR, | |||||
}, | |||||
}; | |||||
}; |
@ -0,0 +1,11 @@ | |||||
import { parseGAT } from "./format/gat"; | |||||
export { parseGAT } from "./format/gat"; | |||||
import { renderGAT } from "./render/gat"; | |||||
export { renderGAT } from "./render/gat"; | |||||
if (typeof window !== "undefined") { | |||||
(window as any).rojs = { | |||||
parseGAT, | |||||
renderGAT, | |||||
}; | |||||
} |
@ -0,0 +1,10 @@ | |||||
precision mediump float; | |||||
// attribute bool walkable; | |||||
const vec4 RED = vec4(1.0, 0.0, 0.0, 1.0); | |||||
const vec4 GREEN = vec4(0.0, 1.0, 0.0, 1.0); | |||||
void main() { | |||||
gl_FragColor = GREEN; | |||||
} |
@ -0,0 +1,142 @@ | |||||
import { mat4, vec3, vec4 } from "gl-matrix"; | |||||
import { Corner, GAT, Tile } from "../format/gat"; | |||||
import { createProgram, createShader, ShaderType } from "../util/render"; | |||||
/* @ts-ignore */ | |||||
import fragmentSource from "./gat.frag"; | |||||
/* @ts-ignore */ | |||||
import vertexSource from "./gat.vert"; | |||||
const UP = vec3.fromValues(0, 1, 0); | |||||
export const renderGAT = (gl: WebGLRenderingContext, gat: GAT) => { | |||||
console.log("gat", gat); | |||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); | |||||
gl.clearColor(0, 0, 0, 1); | |||||
gl.clear(gl.COLOR_BUFFER_BIT); | |||||
const fragmentShader = createShader(gl, ShaderType.FRAGMENT, fragmentSource); | |||||
const vertexShader = createShader(gl, ShaderType.VERTEX, vertexSource); | |||||
const program = createProgram(gl, vertexShader, fragmentShader); | |||||
gl.useProgram(program); | |||||
gl.enable(gl.DEPTH_TEST); | |||||
const maxAltitude = getMaxAltitude(gat); | |||||
console.log("altitude", maxAltitude); | |||||
const camera = vec3.fromValues( | |||||
gat.width / 2, | |||||
maxAltitude + 10, | |||||
gat.height / 2 | |||||
); | |||||
console.log("camera", camera); | |||||
const centerX = Math.floor(gat.width / 2); | |||||
const centerZ = Math.floor(gat.height / 2); | |||||
const centerTile = gat.tiles[gat.width * centerZ + centerX]; | |||||
const center = vec3.fromValues(centerX, avgTileHeight(centerTile), centerZ); | |||||
console.log("center", center); | |||||
const model = mat4.create(); | |||||
mat4.identity(model); | |||||
mat4.translate(model, model, vec3.fromValues(-centerX, 0, -centerZ)); | |||||
const view = mat4.create(); | |||||
mat4.lookAt(view, camera, center, UP); | |||||
const perspective = mat4.create(); | |||||
const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; | |||||
mat4.perspective(perspective, Math.PI / 4, aspect, 0.5, 2000); | |||||
const matrix = mat4.create(); | |||||
mat4.multiply(matrix, view, model); | |||||
mat4.multiply(matrix, perspective, matrix); | |||||
const matrixLoc = gl.getUniformLocation(program, "u_matrix"); | |||||
gl.uniformMatrix4fv(matrixLoc, false, matrix); | |||||
console.log("model", model); | |||||
console.log("view", view); | |||||
console.log("projection", perspective); | |||||
console.log("matrix", matrix); | |||||
const vertices: number[] = []; | |||||
gat.tiles.forEach((tile, i) => { | |||||
const x = i % gat.width; | |||||
const z = Math.floor(i / gat.width); | |||||
const topLeft = [x, tile.altitude[Corner.TOP_LEFT], z]; | |||||
const topRight = [x + 1, tile.altitude[Corner.TOP_RIGHT], z]; | |||||
const bottomLeft = [x, tile.altitude[Corner.BOTTOM_LEFT], z + 1]; | |||||
const bottomRight = [x + 1, tile.altitude[Corner.BOTTOM_RIGHT], z + 1]; | |||||
vertices.push( | |||||
...topLeft, | |||||
...bottomLeft, | |||||
...topRight, | |||||
...topRight, | |||||
...bottomLeft, | |||||
...bottomRight | |||||
); | |||||
}); | |||||
let failCount = 0; | |||||
for (let i = 0; i < vertices.length / 3; ++i) { | |||||
const vec = vec3.fromValues(vertices[i], vertices[i + 1], vertices[i + 2]); | |||||
const result = checkPixelCoord(vec, matrix); | |||||
if (result) { | |||||
if (failCount < 10) { | |||||
console.log(vec); | |||||
console.log(result); | |||||
} | |||||
failCount++; | |||||
} | |||||
} | |||||
console.log("failCount", failCount); | |||||
console.log("totalCount", vertices.length / 3); | |||||
const positionLoc = gl.getAttribLocation(program, "a_position"); | |||||
const vertexBuffer = gl.createBuffer(); | |||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); | |||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); | |||||
gl.enableVertexAttribArray(positionLoc); | |||||
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); | |||||
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0); | |||||
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / 3); | |||||
}; | |||||
const checkPixelCoord = (x: vec3, mat: mat4): vec4 | null => { | |||||
const vec = vec4.fromValues(x[0], x[1], x[2], 1); | |||||
vec4.transformMat4(vec, vec, mat); | |||||
for (let i = 0; i < vec.length; ++i) { | |||||
if (Math.abs(vec[i]) >= 1) { | |||||
return vec; | |||||
} | |||||
} | |||||
return null; | |||||
}; | |||||
const getMaxAltitude = (gat: GAT): number => | |||||
gat.tiles.reduce( | |||||
(max, tile) => | |||||
Object.values(tile.altitude).reduce( | |||||
(max1, altitude) => Math.max(max1, altitude), | |||||
max | |||||
), | |||||
0 | |||||
); | |||||
const avgTileHeight = (tile: Tile): number => { | |||||
const heights = [...Object.values(tile.altitude)]; | |||||
const s = heights.reduce((x, y) => x + y, 0); | |||||
return s / heights.length; | |||||
}; |
@ -0,0 +1,7 @@ | |||||
attribute vec3 a_position; | |||||
uniform mat4 u_matrix; | |||||
void main() { | |||||
gl_Position = u_matrix * vec4(a_position, 1.0); | |||||
} |
@ -0,0 +1,49 @@ | |||||
export enum ShaderType { | |||||
VERTEX, | |||||
FRAGMENT, | |||||
} | |||||
export const createShader = ( | |||||
gl: WebGLRenderingContext, | |||||
type: ShaderType, | |||||
source: string | |||||
): WebGLShader | null => { | |||||
const SHADER_TYPE_TO_GL_TYPE = { | |||||
[ShaderType.VERTEX]: gl.VERTEX_SHADER, | |||||
[ShaderType.FRAGMENT]: gl.FRAGMENT_SHADER, | |||||
}; | |||||
const shader = gl.createShader(SHADER_TYPE_TO_GL_TYPE[type]); | |||||
gl.shaderSource(shader, source); | |||||
gl.compileShader(shader); | |||||
const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); | |||||
if (!success) { | |||||
console.error(gl.getShaderInfoLog(shader)); | |||||
gl.deleteShader(shader); | |||||
return null; | |||||
} | |||||
return shader; | |||||
}; | |||||
export const createProgram = ( | |||||
gl: WebGLRenderingContext, | |||||
vertexShader: WebGLShader, | |||||
fragmentShader: WebGLShader | |||||
): WebGLProgram | null => { | |||||
const program = gl.createProgram(); | |||||
gl.attachShader(program, vertexShader); | |||||
gl.attachShader(program, fragmentShader); | |||||
gl.linkProgram(program); | |||||
const success = gl.getProgramParameter(program, gl.LINK_STATUS); | |||||
if (!success) { | |||||
console.error(gl.getProgramInfoLog(program)); | |||||
gl.deleteProgram(program); | |||||
return null; | |||||
} | |||||
return program; | |||||
}; |
@ -0,0 +1,26 @@ | |||||
// Reads data in little-endian format | |||||
export default class DataScanner { | |||||
offset: number; | |||||
view: DataView; | |||||
constructor(data: DataView | ArrayBuffer) { | |||||
this.offset = 0; | |||||
this.view = data instanceof DataView ? data : new DataView(data); | |||||
} | |||||
float32(): number { | |||||
const x = this.view.getFloat32(this.offset, true); | |||||
this.offset += 4; | |||||
return x; | |||||
} | |||||
uint8(): number { | |||||
return this.view.getUint8(this.offset++); | |||||
} | |||||
uint32(): number { | |||||
const x = this.view.getUint32(this.offset, true); | |||||
this.offset += 4; | |||||
return x; | |||||
} | |||||
} |