mdx-m3-viewer
Version:
A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.
1,081 lines (878 loc) • 37 kB
JavaScript
import {vec3, quat} from 'gl-matrix';
import {VEC3_UNIT_Z} from '../../../common/gl-matrix-addon';
import unique from '../../../common/arrayunique';
import War3Map from '../../../parsers/w3x/map';
import War3MapW3i from '../../../parsers/w3x/w3i/file';
import War3MapW3e from '../../../parsers/w3x/w3e/file';
import War3MapDoo from '../../../parsers/w3x/doo/file';
import War3MapUnitsDoo from '../../../parsers/w3x/unitsdoo/file';
import MpqArchive from '../../../parsers/mpq/archive';
import MappedData from '../../../utils/mappeddata';
import ModelViewer from '../../viewer';
import Grid from '../../grid';
import geoHandler from '../geo/handler';
import mdxHandler from '../mdx/handler';
import shaders from './shaders';
import getCliffVariation from './variations';
import TerrainModel from './terrainmodel';
// import SimpleModel from './simplemodel';
import standOnRepeat from './standsequence';
import Unit from './unit';
let normalHeap1 = vec3.create();
let normalHeap2 = vec3.create();
/**
*
*/
export default class War3MapViewer extends ModelViewer {
/**
* @param {HTMLCanvasElement} canvas
* @param {function} wc3PathSolver
*/
constructor(canvas, wc3PathSolver) {
super(canvas);
this.batchSize = 64;
this.on('error', (target, error, reason) => console.error(target, error, reason));
this.addHandler(geoHandler);
this.addHandler(mdxHandler);
/** @member {function} */
this.wc3PathSolver = wc3PathSolver;
this.groundShader = this.loadShader('Ground', shaders.vsGround, shaders.fsGround);
this.waterShader = this.loadShader('Water', shaders.vsWater, shaders.fsWater);
this.cliffShader = this.loadShader('Cliffs', shaders.vsCliffs, shaders.fsCliffs);
this.simpleModelShader = this.loadShader('SimpleModel', shaders.vsSimpleModel, shaders.fsSimpleModel);
this.scene = this.addScene();
this.camera = this.scene.camera;
this.waterIndex = 0;
this.waterIncreasePerFrame = 0;
this.anyReady = false;
this.terrainCliffsAndWaterLoaded = false;
this.terrainData = new MappedData();
this.cliffTypesData = new MappedData();
this.waterData = new MappedData();
this.terrainReady = false;
this.cliffsReady = false;
this.whenLoaded(['TerrainArt\\Terrain.slk', 'TerrainArt\\CliffTypes.slk', 'TerrainArt\\Water.slk'].map((path) => this.loadGeneric(wc3PathSolver(path)[0], 'text')))
.then(([terrain, cliffTypes, water]) => {
this.terrainCliffsAndWaterLoaded = true;
this.terrainData.load(terrain.data);
this.cliffTypesData.load(cliffTypes.data);
this.waterData.load(water.data);
this.emit('terrainloaded');
});
this.doodadsAndDestructiblesLoaded = false;
this.doodadsData = new MappedData();
this.doodadMetaData = new MappedData();
this.destructableMetaData = new MappedData();
this.doodads = [];
this.terrainDoodads = [];
this.doodadsReady = false;
this.whenLoaded(['Doodads\\Doodads.slk', 'Doodads\\DoodadMetaData.slk', 'Units\\DestructableData.slk', 'Units\\DestructableMetaData.slk'].map((path) => this.loadGeneric(wc3PathSolver(path)[0], 'text')))
.then(([doodads, doodadMetaData, destructableData, destructableMetaData]) => {
this.doodadsAndDestructiblesLoaded = true;
this.doodadsData.load(doodads.data);
this.doodadMetaData.load(doodadMetaData.data);
this.doodadsData.load(destructableData.data);
this.destructableMetaData.load(destructableMetaData.data);
this.emit('doodadsloaded');
});
this.unitsAndItemsLoaded = false;
this.unitsData = new MappedData();
this.unitMetaData = new MappedData();
this.units = [];
this.unitsReady = false;
this.whenLoaded(['Units\\UnitData.slk', 'Units\\unitUI.slk', 'Units\\ItemData.slk', 'Units\\UnitMetaData.slk'].map((path) => this.loadGeneric(wc3PathSolver(path)[0], 'text')))
.then(([unitData, unitUi, itemData, unitMetaData]) => {
this.unitsAndItemsLoaded = true;
this.unitsData.load(unitData.data);
this.unitsData.load(unitUi.data);
this.unitsData.load(itemData.data);
this.unitMetaData.load(unitMetaData.data);
this.emit('unitsloaded');
});
}
/**
*
*/
renderGround() {
if (this.terrainReady) {
let gl = this.gl;
let webgl = this.webgl;
let instancedArrays = webgl.extensions.instancedArrays;
let shader = this.groundShader;
let uniforms = shader.uniforms;
let attribs = shader.attribs;
let {columns, rows, centerOffset, vertexBuffer, faceBuffer, heightMap, instanceBuffer, instanceCount, textureBuffer, variationBuffer} = this.terrainRenderData;
let tilesetTextures = this.tilesetTextures;
let instanceAttrib = attribs.a_InstanceID;
let positionAttrib = attribs.a_position;
let texturesAttrib = attribs.a_textures;
let variationsAttrib = attribs.a_variations;
gl.disable(gl.BLEND);
webgl.useShaderProgram(shader);
gl.uniformMatrix4fv(uniforms.u_mvp, false, this.camera.worldProjectionMatrix);
gl.uniform2fv(uniforms.u_offset, centerOffset);
gl.uniform2f(uniforms.u_size, columns - 1, rows - 1);
gl.uniform1i(uniforms.u_heightMap, 0);
gl.uniform1i(uniforms.u_tilesets, 1);
gl.uniform1f(uniforms.u_tilesetHeight, 1 / (tilesetTextures.length + 1));
gl.uniform1f(uniforms.u_tilesetCount, tilesetTextures.length + 1);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, heightMap);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.tilesetsTexture);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 8, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
gl.vertexAttribPointer(instanceAttrib, 1, gl.FLOAT, false, 4, 0);
instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.vertexAttribPointer(texturesAttrib, 4, gl.UNSIGNED_BYTE, false, 4, 0);
instancedArrays.vertexAttribDivisorANGLE(texturesAttrib, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, variationBuffer);
gl.vertexAttribPointer(variationsAttrib, 4, gl.UNSIGNED_BYTE, false, 4, 0);
instancedArrays.vertexAttribDivisorANGLE(variationsAttrib, 1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuffer);
instancedArrays.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, instanceCount);
instancedArrays.vertexAttribDivisorANGLE(texturesAttrib, 0);
instancedArrays.vertexAttribDivisorANGLE(variationsAttrib, 0);
instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 0);
}
}
/**
*
*/
renderWater() {
if (this.terrainReady) {
let gl = this.gl;
let webgl = this.webgl;
let instancedArrays = webgl.extensions.instancedArrays;
let shader = this.waterShader;
let uniforms = shader.uniforms;
let attribs = shader.attribs;
let {columns, rows, centerOffset, vertexBuffer, faceBuffer, heightMap, instanceBuffer, instanceCount, waterHeightMap, waterBuffer} = this.terrainRenderData;
let instanceAttrib = attribs.a_InstanceID;
let positionAttrib = attribs.a_position;
let isWaterAttrib = attribs.a_isWater;
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
webgl.useShaderProgram(shader);
gl.uniformMatrix4fv(uniforms.u_mvp, false, this.camera.worldProjectionMatrix);
gl.uniform2fv(uniforms.u_offset, centerOffset);
gl.uniform2f(uniforms.u_size, columns - 1, rows - 1);
gl.uniform1i(uniforms.u_heightMap, 0);
gl.uniform1i(uniforms.u_waterHeightMap, 1);
gl.uniform1i(uniforms.u_waterMap, 2);
gl.uniform1f(uniforms.u_offsetHeight, this.waterHeightOffset);
gl.uniform1f(uniforms.u_tileIndex, this.waterIndex | 0);
gl.uniform4fv(uniforms.u_maxDeepColor, this.maxDeepColor);
gl.uniform4fv(uniforms.u_minDeepColor, this.minDeepColor);
gl.uniform4fv(uniforms.u_maxShallowColor, this.maxShallowColor);
gl.uniform4fv(uniforms.u_minShallowColor, this.minShallowColor);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, heightMap);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, waterHeightMap);
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.waterTexture);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 8, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
gl.vertexAttribPointer(instanceAttrib, 1, gl.FLOAT, false, 4, 0);
instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, waterBuffer);
gl.vertexAttribPointer(isWaterAttrib, 1, gl.UNSIGNED_BYTE, false, 1, 0);
instancedArrays.vertexAttribDivisorANGLE(isWaterAttrib, 1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuffer);
instancedArrays.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, instanceCount);
instancedArrays.vertexAttribDivisorANGLE(isWaterAttrib, 0);
instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 0);
}
}
/**
*
*/
renderCliffs() {
if (this.cliffsReady) {
let gl = this.gl;
let instancedArrays = gl.extensions.instancedArrays;
let webgl = this.webgl;
let shader = this.cliffShader;
let attribs = shader.attribs;
let uniforms = shader.uniforms;
let {centerOffset, cliffHeightMap, heightMapSize} = this.terrainRenderData;
gl.disable(gl.BLEND);
webgl.useShaderProgram(shader);
gl.uniformMatrix4fv(uniforms.u_mvp, false, this.camera.worldProjectionMatrix);
gl.uniform1i(uniforms.u_heightMap, 0);
gl.uniform2fv(uniforms.u_pixel, heightMapSize);
gl.uniform2fv(uniforms.u_centerOffset, centerOffset);
gl.uniform1i(uniforms.u_texture1, 1);
gl.uniform1i(uniforms.u_texture2, 2);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, cliffHeightMap);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.cliffTextures[0].webglResource);
if (this.cliffTextures.length > 1) {
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.cliffTextures[1].webglResource);
}
// Set instanced attributes.
if (!gl.extensions.vertexArrayObject) {
instancedArrays.vertexAttribDivisorANGLE(attribs.a_instancePosition, 1);
instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceTexture, 1);
}
// Render the cliffs.
for (let cliff of this.cliffModels) {
cliff.render(gl, instancedArrays, attribs);
}
// Clear instanced attributes.
if (!gl.extensions.vertexArrayObject) {
instancedArrays.vertexAttribDivisorANGLE(attribs.a_instancePosition, 0);
instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceTexture, 0);
}
}
}
// /**
// *
// */
// renderDoodads(opaque) {
// if (this.doodadsReady) {
// let gl = this.gl;
// let instancedArrays = gl.extensions.instancedArrays;
// let webgl = this.webgl;
// let shader = this.simpleModelShader;
// let attribs = shader.attribs;
// let uniforms = shader.uniforms;
// webgl.useShaderProgram(shader);
// gl.uniformMatrix4fv(uniforms.u_mvp, false, this.camera.worldProjectionMatrix);
// gl.uniform1i(uniforms.u_texture, 0);
// gl.activeTexture(gl.TEXTURE0);
// // Enable instancing.
// instancedArrays.vertexAttribDivisorANGLE(attribs.a_instancePosition, 1);
// instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceRotation, 1);
// instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceScale, 1);
// // Render the dooadads.
// for (let doodad of this.doodads) {
// if (opaque) {
// doodad.renderOpaque(gl, instancedArrays, uniforms, attribs);
// } else {
// doodad.renderTranslucent(gl, instancedArrays, uniforms, attribs);
// }
// }
// // Render the terrain doodads.
// for (let doodad of this.terrainDoodads) {
// if (opaque) {
// doodad.renderOpaque(gl, instancedArrays, uniforms, attribs);
// } else {
// doodad.renderTranslucent(gl, instancedArrays, uniforms, attribs);
// }
// }
// // Disable instancing.
// instancedArrays.vertexAttribDivisorANGLE(attribs.a_instancePosition, 0);
// instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceRotation, 0);
// instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceScale, 0);
// }
// }
/**
* Render the map.
*/
render() {
if (this.anyReady) {
this.gl.viewport(...this.camera.rect);
this.renderGround();
this.renderCliffs();
// this.renderDoodads(true);
super.renderOpaque();
// this.renderDoodads(false);
super.renderTranslucent();
this.renderWater();
}
}
/**
* Update the map.
*/
update() {
if (this.anyReady) {
this.waterIndex += this.waterIncreasePerFrame;
if (this.waterIndex >= this.waterTextures.length) {
this.waterIndex = 0;
}
super.update();
}
}
/**
* @param {ArrayBuffer} buffer
*/
async loadMap(buffer) {
// Readonly mode to optimize memory usage.
this.mapMpq = new War3Map(buffer, true);
let wc3PathSolver = this.wc3PathSolver;
let w3i = new War3MapW3i(this.mapMpq.get('war3map.w3i').arrayBuffer());
let tileset = w3i.tileset;
this.emit('maploaded');
this.tilesetMpq = new MpqArchive((await this.loadGeneric(wc3PathSolver(`${tileset}.mpq`)[0], 'arrayBuffer').whenLoaded()).data, true);
this.mapPathSolver = (path) => {
// MPQ paths have backwards slashes...always? Don't know.
let mpqPath = path.replace(/\//g, '\\');
// If the file is in the map, return it.
// Otherwise, if it's in the tileset MPQ, return it from there.
let file = this.mapMpq.get(mpqPath) || this.tilesetMpq.get(mpqPath);
if (file) {
return [file.arrayBuffer(), path.substr(path.lastIndexOf('.')), false];
}
// Try to get the file from the game MPQs.
return wc3PathSolver(path);
};
let w3e = new War3MapW3e(this.mapMpq.get('war3map.w3e').arrayBuffer());
this.corners = w3e.corners;
this.centerOffset = w3e.centerOffset;
this.mapSize = w3e.mapSize;
// Override the grid based on the map.
this.scene.grid = new Grid(this.centerOffset, [this.mapSize[0] * 128 - 128, this.mapSize[1] * 128 - 128], [16 * 128, 16 * 128]);
this.emit('tilesetloaded');
if (this.terrainCliffsAndWaterLoaded) {
this.loadTerrainCliffsAndWater(w3e);
} else {
this.once('terrainloaded', () => this.loadTerrainCliffsAndWater(w3e));
}
let modifications = this.mapMpq.readModifications();
if (this.doodadsAndDestructiblesLoaded) {
this.loadDoodadsAndDestructibles(modifications);
} else {
this.once('doodadsloaded', () => this.loadDoodadsAndDestructibles(modifications));
}
if (this.unitsAndItemsLoaded) {
this.loadUnitsAndItems(modifications);
} else {
this.once('unitsloaded', () => this.loadUnitsAndItems(modifications));
}
}
/**
* @param {*} modifications
*/
loadDoodadsAndDestructibles(modifications) {
this.applyModificationFile(this.doodadsData, this.doodadMetaData, modifications.w3d);
this.applyModificationFile(this.doodadsData, this.destructableMetaData, modifications.w3b);
let doo = new War3MapDoo(this.mapMpq.get('war3map.doo').arrayBuffer());
let scene = this.scene;
// Collect the doodad and destructible data.
for (let doodad of doo.doodads) {
let row = this.doodadsData.getRow(doodad.id);
let file = row.file;
let numVar = row.numVar;
if (file.endsWith('.mdl')) {
file = file.slice(0, -4);
}
let fileVar = file;
file += '.mdx';
if (numVar > 1) {
fileVar += Math.min(doodad.variation, numVar - 1);
}
fileVar += '.mdx';
// First see if the model is local.
// Doodads refering to local models may have invalid variations, so if the variation doesn't exist, try without a variation.
let mpqFile = this.mapMpq.get(fileVar) || this.mapMpq.get(file);
let model;
if (mpqFile) {
model = this.load(mpqFile.name);
} else {
model = this.load(fileVar);
}
let instance = model.addInstance();
instance.move(doodad.location);
instance.rotateLocal(quat.setAxisAngle(quat.create(), VEC3_UNIT_Z, doodad.angle));
instance.scale(doodad.scale);
instance.setScene(scene);
standOnRepeat(instance);
}
this.doodadsReady = true;
this.anyReady = true;
}
/**
* @param {*} modifications
*/
loadUnitsAndItems(modifications) {
this.applyModificationFile(this.unitsData, this.unitMetaData, modifications.w3u);
this.applyModificationFile(this.unitsData, this.unitMetaData, modifications.w3t);
let unitsDoo = new War3MapUnitsDoo(this.mapMpq.get('war3mapUnits.doo').arrayBuffer());
// Collect the units and items data.
for (let unit of unitsDoo.units) {
this.units.push(new Unit(this, unit));
}
this.unitsReady = true;
this.anyReady = true;
}
/**
*
*/
async loadTerrainCliffsAndWater(w3e) {
let tileset = w3e.tileset;
this.tilesets = [];
this.tilesetTextures = [];
for (let groundTileset of w3e.groundTilesets) {
let row = this.terrainData.getRow(groundTileset);
this.tilesets.push(row);
this.tilesetTextures.push(this.load(`${row.dir}\\${row.file}.blp`));
}
let blights = {
A: 'Ashen',
B: 'Barrens',
C: 'Felwood',
D: 'Cave',
F: 'Lordf',
G: 'Dungeon',
I: 'Ice',
J: 'DRuins',
K: 'Citadel',
L: 'Lords',
N: 'North',
O: 'Outland',
Q: 'VillageFall',
V: 'Village',
W: 'Lordw',
X: 'Village',
Y: 'Village',
Z: 'Ruins',
};
this.blightTextureIndex = this.tilesetTextures.length;
this.tilesetTextures.push(this.load(`TerrainArt\\Blight\\${blights[tileset]}_Blight.blp`));
this.cliffTilesets = [];
this.cliffTextures = [];
for (let cliffTileset of w3e.cliffTilesets) {
let row = this.cliffTypesData.getRow(cliffTileset);
this.cliffTilesets.push(row);
this.cliffTextures.push(this.load(`${row.texDir}\\${row.texFile}.blp`));
}
let waterRow = this.waterData.getRow(`${tileset}Sha`);
this.waterHeightOffset = waterRow.height;
this.waterIncreasePerFrame = waterRow.texRate / 60;
this.waterTextures = [];
this.maxDeepColor = new Float32Array([waterRow.Dmax_R, waterRow.Dmax_G, waterRow.Dmax_B, waterRow.Dmax_A]);
this.minDeepColor = new Float32Array([waterRow.Dmin_R, waterRow.Dmin_G, waterRow.Dmin_B, waterRow.Dmin_A]);
this.maxShallowColor = new Float32Array([waterRow.Smax_R, waterRow.Smax_G, waterRow.Smax_B, waterRow.Smax_A]);
this.minShallowColor = new Float32Array([waterRow.Smin_R, waterRow.Smin_G, waterRow.Smin_B, waterRow.Smin_A]);
for (let i = 0, l = waterRow.numTex; i < l; i++) {
this.waterTextures.push(this.load(`${waterRow.texFile}${i < 10 ? '0' : ''}${i}.blp`));
}
await this.whenLoaded([...this.tilesetTextures, ...this.waterTextures]);
let gl = this.gl;
this.createTilesetsAndWaterTextures();
let corners = w3e.corners;
let [columns, rows] = this.mapSize;
let centerOffset = this.centerOffset;
let instanceCount = (columns - 1) * (rows - 1);
let cliffHeights = new Float32Array(columns * rows);
let cornerHeights = new Float32Array(columns * rows);
let waterHeights = new Float32Array(columns * rows);
let cornerTextures = new Uint8Array(instanceCount * 4);
let cornerVariations = new Uint8Array(instanceCount * 4);
let waterFlags = new Uint8Array(instanceCount);
let instance = 0;
let cliffs = {};
this.columns = columns - 1;
this.rows = rows - 1;
for (let y = 0; y < rows; y++) {
for (let x = 0; x < columns; x++) {
let bottomLeft = corners[y][x];
let index = y * columns + x;
cliffHeights[index] = bottomLeft.groundHeight;
cornerHeights[index] = bottomLeft.groundHeight + bottomLeft.layerHeight - 2;
waterHeights[index] = bottomLeft.waterHeight;
if (y < rows - 1 && x < columns - 1) {
// Water can be used with cliffs and normal corners, so store water state regardless.
waterFlags[instance] = this.isWater(x, y);
// Is this a cliff, or a normal corner?
if (this.isCliff(x, y)) {
let bottomLeftLayer = bottomLeft.layerHeight;
let bottomRightLayer = corners[y][x + 1].layerHeight;
let topLeftLayer = corners[y + 1][x].layerHeight;
let topRightLayer = corners[y + 1][x + 1].layerHeight;
let base = Math.min(bottomLeftLayer, bottomRightLayer, topLeftLayer, topRightLayer);
let fileName = this.cliffFileName(bottomLeftLayer, bottomRightLayer, topLeftLayer, topRightLayer, base);
if (fileName !== 'AAAA') {
let cliffTexture = bottomLeft.cliffTexture;
/// ?
if (cliffTexture === 15) {
cliffTexture = 1;
}
let cliffRow = this.cliffTilesets[cliffTexture];
let dir = cliffRow.cliffModelDir;
let path = `Doodads\\Terrain\\${dir}\\${dir}${fileName}${getCliffVariation(dir, fileName, bottomLeft.cliffVariation)}.mdx`;
if (!cliffs[path]) {
cliffs[path] = {locations: [], textures: []};
}
cliffs[path].locations.push((x + 1) * 128 + centerOffset[0], y * 128 + centerOffset[1], (base - 2) * 128);
cliffs[path].textures.push(cliffTexture);
}
} else {
let bottomLeftTexture = this.cornerTexture(x, y);
let bottomRightTexture = this.cornerTexture(x + 1, y);
let topLeftTexture = this.cornerTexture(x, y + 1);
let topRightTexture = this.cornerTexture(x + 1, y + 1);
let textures = unique([bottomLeftTexture, bottomRightTexture, topLeftTexture, topRightTexture]).sort();
cornerTextures[instance * 4] = textures[0] + 1;
cornerVariations[instance * 4] = this.getVariation(textures[0], bottomLeft.groundVariation);
textures.shift();
for (let i = 0, l = textures.length; i < l; i++) {
let texture = textures[i];
let bitset = 0;
if (bottomRightTexture === texture) {
bitset |= 0b0001;
}
if (bottomLeftTexture === texture) {
bitset |= 0b0010;
}
if (topRightTexture === texture) {
bitset |= 0b0100;
}
if (topLeftTexture === texture) {
bitset |= 0b1000;
}
cornerTextures[instance * 4 + 1 + i] = texture + 1;
cornerVariations[instance * 4 + 1 + i] = bitset;
}
}
instance += 1;
}
}
}
let vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);
let faceBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, faceBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0, 1, 2, 1, 3, 2]), gl.STATIC_DRAW);
let cliffHeightMap = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, cliffHeightMap);
this.webgl.setTextureMode(gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, columns, rows, 0, gl.ALPHA, gl.FLOAT, cliffHeights);
let heightMap = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, heightMap);
this.webgl.setTextureMode(gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, columns, rows, 0, gl.ALPHA, gl.FLOAT, cornerHeights);
let waterHeightMap = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, waterHeightMap);
this.webgl.setTextureMode(gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.NEAREST, gl.NEAREST);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, columns, rows, 0, gl.ALPHA, gl.FLOAT, waterHeights);
let instanceBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(instanceCount).map((currentValue, index, array) => index), gl.STATIC_DRAW);
let textureBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, cornerTextures, gl.STATIC_DRAW);
let variationBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, variationBuffer);
gl.bufferData(gl.ARRAY_BUFFER, cornerVariations, gl.STATIC_DRAW);
let waterBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, waterBuffer);
gl.bufferData(gl.ARRAY_BUFFER, waterFlags, gl.STATIC_DRAW);
let heightMapSize = new Float32Array([1 / columns, 1 / rows]);
this.terrainRenderData = {
rows,
columns,
centerOffset,
vertexBuffer,
faceBuffer,
heightMap,
instanceBuffer,
instanceCount,
cornerTextures,
textureBuffer,
variationBuffer,
heightMapSize,
cliffHeightMap,
waterHeightMap,
waterBuffer,
};
this.terrainReady = true;
this.anyReady = true;
let cliffPromises = Object.entries(cliffs).map((cliff) => {
let path = cliff[0];
let {locations, textures} = cliff[1];
return this.loadGeneric(this.mapPathSolver(path)[0], 'arrayBuffer')
.whenLoaded()
.then((resource) => {
return new TerrainModel(gl, resource.data, locations, textures, this.cliffShader.attribs);
});
});
this.cliffModels = await Promise.all(cliffPromises);
this.cliffsReady = true;
}
/**
* @param {number} bottomLeftLayer
* @param {number} bottomRightLayer
* @param {number} topLeftLayer
* @param {number} topRightLayer
* @param {number} base
* @return {string}
*/
cliffFileName(bottomLeftLayer, bottomRightLayer, topLeftLayer, topRightLayer, base) {
return String.fromCharCode(65 + bottomLeftLayer - base) +
String.fromCharCode(65 + topLeftLayer - base) +
String.fromCharCode(65 + topRightLayer - base) +
String.fromCharCode(65 + bottomRightLayer - base);
}
/**
* Creates a shared texture that holds all of the tileset textures.
* Each tileset is flattend to a single row of tiles, such that indices 0-15 are the normal part, and indices 16-31 are the extended part.
*/
createTilesetsAndWaterTextures() {
let tilesets = this.tilesetTextures;
let tilesetsCount = tilesets.length;
let canvas = document.createElement('canvas');
let ctx = canvas.getContext('2d');
canvas.width = 2048;
canvas.height = 64 * (tilesetsCount + 1); // 1 is added for a black tileset, to remove branches from the fragment shader, at the cost of 512Kb.
for (let tileset = 0; tileset < tilesetsCount; tileset++) {
let imageData = tilesets[tileset].imageData;
for (let variation = 0; variation < 16; variation++) {
let x = (variation % 4) * 64;
let y = ((variation / 4) | 0) * 64;
ctx.putImageData(imageData, variation * 64 - x, (tileset + 1) * 64 - y, x, y, 64, 64);
}
if (imageData.width === 512) {
for (let variation = 0; variation < 16; variation++) {
let x = 256 + (variation % 4) * 64;
let y = ((variation / 4) | 0) * 64;
ctx.putImageData(imageData, 1024 + variation * 64 - x, (tileset + 1) * 64 - y, x, y, 64, 64);
}
}
}
let gl = this.gl;
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
this.webgl.setTextureMode(gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.LINEAR, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
this.tilesetsTexture = texture;
canvas.height = 128 * 3; // up to 48 frames.
let waterTextures = this.waterTextures;
for (let i = 0, l = waterTextures.length; i < l; i++) {
let x = i % 16;
let y = (i / 16) | 0;
ctx.putImageData(waterTextures[i].imageData, x * 128, y * 128);
}
let waterTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, waterTexture);
this.webgl.setTextureMode(gl.CLAMP_TO_EDGE, gl.CLAMP_TO_EDGE, gl.LINEAR, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas);
this.waterTexture = waterTexture;
}
/**
* @param {number} groundTexture
* @param {number} variation
* @return {number}
*/
getVariation(groundTexture, variation) {
let texture = this.tilesetTextures[groundTexture];
// Extended?
if (texture.width > texture.height) {
if (variation < 16) {
return 16 + variation;
} else if (variation === 16) {
return 15;
} else {
return 0;
}
} else {
if (variation === 0) {
return 0;
} else {
return 15;
}
}
}
/**
* Is the corner at the given column and row a cliff?
*
* @param {number} column
* @param {number} row
* @return {boolean}
*/
isCliff(column, row) {
if (column < 1 || column > this.columns - 1 || row < 1 || row > this.rows - 1) {
return false;
}
let corners = this.corners;
let bottomLeft = corners[row][column].layerHeight;
let bottomRight = corners[row][column + 1].layerHeight;
let topLeft = corners[row + 1][column].layerHeight;
let topRight = corners[row + 1][column + 1].layerHeight;
return bottomLeft !== bottomRight || bottomLeft !== topLeft || bottomLeft !== topRight;
}
/**
* Is the tile at the given column and row water?
*
* @param {number} column
* @param {number} row
* @return {boolean}
*/
isWater(column, row) {
let corners = this.corners;
return corners[row][column].water || corners[row][column + 1].water || corners[row + 1][column].water || corners[row + 1][column + 1].water;
}
/**
* Given a cliff index, get its ground texture index.
* This is an index into the tilset textures.
*
* @param {number} whichCliff
* @return {number}
*/
cliffGroundIndex(whichCliff) {
let whichTileset = this.cliffTilesets[whichCliff].groundTile;
let tilesets = this.tilesets;
for (let i = 0, l = tilesets.length; i < l; i++) {
if (tilesets[i].tileID === whichTileset) {
return i;
}
}
}
/**
* Get the ground texture of a corner, whether it's normal ground, a cliff, or a blighted corner.
*
* @param {number} column
* @param {number} row
* @return {number}
*/
cornerTexture(column, row) {
let corners = this.corners;
let columns = this.columns;
let rows = this.rows;
for (let y = -1; y < 1; y++) {
for (let x = -1; x < 1; x++) {
if (column + x > 0 && column + x < columns - 1 && row + y > 0 && row + y < rows - 1) {
if (this.isCliff(column + x, row + y)) {
let texture = corners[row + y][column + x].cliffTexture;
if (texture === 15) {
texture = 1;
}
return this.cliffGroundIndex(texture);
}
}
}
}
let corner = corners[row][column];
// Is this corner blighted?
if (corner.blight) {
return this.blightTextureIndex;
}
return corner.groundTexture;
}
/**
* @param {*} src
* @param {?function} pathSolver
* @param {?Object} options
* @return {Resource}
*/
load(src, pathSolver, options) {
return super.load(src, pathSolver || this.mapPathSolver, options);
}
/**
*
* @param {*} dataMap
* @param {*} metadataMap
* @param {*} modificationFile
*/
applyModificationFile(dataMap, metadataMap, modificationFile) {
if (modificationFile) {
// Modifications to built-in objects
this.applyModificationTable(dataMap, metadataMap, modificationFile.originalTable);
// Declarations of user-defined objects
this.applyModificationTable(dataMap, metadataMap, modificationFile.customTable);
}
}
/**
*
* @param {*} dataMap
* @param {*} metadataMap
* @param {*} modificationTable
*/
applyModificationTable(dataMap, metadataMap, modificationTable) {
for (let modificationObject of modificationTable.objects) {
let row;
if (modificationObject.newId !== '') {
row = dataMap.getRow(modificationObject.newId);
if (!row) {
row = {...dataMap.getRow(modificationObject.oldId)};
dataMap.setRow(modificationObject.newId, row);
}
} else {
row = dataMap.getRow(modificationObject.oldId);
}
for (let modification of modificationObject.modifications) {
let metadata = metadataMap.getRow(modification.id);
if (metadata) {
row[metadata.field] = modification.value;
} else {
console.warn('Unknown modification ID', modification);
}
}
}
}
/**
*
* @param {Float32Array} out
* @param {number} x
* @param {number} y
* @return {out}
*/
groundNormal(out, x, y) {
let centerOffset = this.centerOffset;
let mapSize = this.mapSize;
x = (x - centerOffset[0]) / 128;
y = (y - centerOffset[1]) / 128;
let cellX = x | 0;
let cellY = y | 0;
// See if this coordinate is in the map
if (cellX >= 0 && cellX < mapSize[0] - 1 && cellY >= 0 && cellY < mapSize[1] - 1) {
// See http://gamedev.stackexchange.com/a/24574
let corners = this.corners;
let bottomLeft = corners[cellY][cellX].groundHeight;
let bottomRight = corners[cellY][cellX + 1].groundHeight;
let topLeft = corners[cellY + 1][cellX].groundHeight;
let topRight = corners[cellY + 1][cellX + 1].groundHeight;
let sqX = x - cellX;
let sqY = y - cellY;
if (sqX + sqY < 1) {
vec3.set(normalHeap1, 1, 0, bottomRight - bottomLeft);
vec3.set(normalHeap2, 0, 1, topLeft - bottomLeft);
} else {
vec3.set(normalHeap1, -1, 0, topRight - topLeft);
vec3.set(normalHeap2, 0, 1, topRight - bottomRight);
}
vec3.normalize(out, vec3.cross(out, normalHeap1, normalHeap2));
} else {
vec3.set(out, 0, 0, 1);
}
return out;
}
}
/*
heightAt(location) {
let heightMap = this.heightMap,
offset = this.offset,
x = (location[0] / 128) + offset[0],
y = (location[1] / 128) + offset[1];
let minY = Math.floor(y),
maxY = Math.ceil(y),
minX = Math.floor(x),
maxX = Math.ceil(x);
// See if this coordinate is in the map
if (maxY > 0 && minY < heightMap.length - 1 && maxX > 0 && minX < heightMap[0].length - 1) {
// See http://gamedev.stackexchange.com/a/24574
let triZ0 = heightMap[minY][minX],
triZ1 = heightMap[minY][maxX],
triZ2 = heightMap[maxY][minX],
triZ3 = heightMap[maxY][maxX],
sqX = x - minX,
sqZ = y - minY,
height;
if ((sqX + sqZ) < 1) {
height = triZ0 + (triZ1 - triZ0) * sqX + (triZ2 - triZ0) * sqZ;
} else {
height = triZ3 + (triZ1 - triZ3) * (1 - sqZ) + (triZ2 - triZ3) * (1 - sqX);
}
return height * 128;
}
return 0;
}
*/