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