Browse Source

First stab at GAT rendering

master
Forest Belton 2 years ago
parent
commit
a9c230c9a9
15 changed files with 429 additions and 7357 deletions
  1. +32
    -0
      dist/example.js
  2. +23
    -0
      dist/index.html
  3. BIN
      dist/prt_fild08.gat
  4. BIN
      dist/prt_fild08.gnd
  5. +0
    -0
      index.ts
  6. +100
    -0
      lib/format/gat.ts
  7. +11
    -0
      lib/index.ts
  8. +10
    -0
      lib/render/gat.frag
  9. +142
    -0
      lib/render/gat.ts
  10. +7
    -0
      lib/render/gat.vert
  11. +49
    -0
      lib/util/render.ts
  12. +26
    -0
      lib/util/scanner.ts
  13. +12
    -7355
      package-lock.json
  14. +5
    -0
      package.json
  15. +12
    -2
      webpack.config.js

+ 32
- 0
dist/example.js View File

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

+ 23
- 0
dist/index.html View 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>

BIN
dist/prt_fild08.gat View File


BIN
dist/prt_fild08.gnd View File


+ 0
- 0
index.ts View File


+ 100
- 0
lib/format/gat.ts View File

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

+ 11
- 0
lib/index.ts View File

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

+ 10
- 0
lib/render/gat.frag View File

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

+ 142
- 0
lib/render/gat.ts View File

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

+ 7
- 0
lib/render/gat.vert View File

@ -0,0 +1,7 @@
attribute vec3 a_position;
uniform mat4 u_matrix;
void main() {
gl_Position = u_matrix * vec4(a_position, 1.0);
}

+ 49
- 0
lib/util/render.ts View File

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

+ 26
- 0
lib/util/scanner.ts View File

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

+ 12
- 7355
package-lock.json
File diff suppressed because it is too large
View File


+ 5
- 0
package.json View File

@ -4,6 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
@ -15,5 +17,8 @@
"webpack": "^5.58.2",
"webpack-cli": "^4.9.0",
"webpack-dev-server": "^4.3.1"
},
"dependencies": {
"gl-matrix": "^3.4.3"
}
}

+ 12
- 2
webpack.config.js View File

@ -3,11 +3,13 @@ const webpack = require("webpack");
module.exports = {
devServer: {
contentBase: path.join(__dirname, "dist"),
port: 9000,
static: {
directory: path.join(__dirname, "dist"),
},
},
devtool: "inline-source-map",
entry: ["./index.ts"],
entry: ["./lib/index.ts"],
mode: process.env.NODE_ENV === "production" ? "production" : "development",
module: {
rules: [
@ -16,10 +18,15 @@ module.exports = {
exclude: /node_modules/,
use: "ts-loader",
},
{
test: /\.(frag|vert)$/,
type: "asset/source",
},
],
},
output: {
path: path.join(__dirname, "dist", "build"),
publicPath: "/build",
filename: "bundle.js",
},
performance: {
@ -27,4 +34,7 @@ module.exports = {
maxAssetSize: 512000,
},
plugins: [new webpack.EnvironmentPlugin({ NODE_ENV: "development" })],
resolve: {
extensions: [".js", ".ts"],
},
};

Loading…
Cancel
Save