UNPKG

mdx-m3-viewer

Version:

A browser WebGL model viewer. Mainly focused on models of the games Warcraft 3 and Starcraft 2.

1,067 lines (844 loc) 37.7 kB
import { vec3 } from 'gl-matrix'; import unique from '../../../common/arrayunique'; import urlWithParams from '../../../common/urlwithparams'; import { FetchDataTypeName } from '../../../common/fetchdatatype'; import { MappedData } from '../../../utils/mappeddata'; import War3Map from '../../../parsers/w3x/map'; import War3MapW3i from '../../../parsers/w3x/w3i/file'; import War3MapW3e from '../../../parsers/w3x/w3e/file'; import Corner from '../../../parsers/w3x/w3e/corner'; import War3MapDoo from '../../../parsers/w3x/doo/file'; import War3MapUnitsDoo from '../../../parsers/w3x/unitsdoo/file'; import ModelViewer from '../../viewer'; import ShaderProgram from '../../gl/program'; import Scene from '../../scene'; import Grid from '../../grid'; import { PathSolver } from '../../handlerresource'; import Texture from '../../texture'; import mdxHandler from '../mdx/handler'; import MdxModel from '../mdx/model'; import MdxModelInstance from '../mdx/modelinstance'; import getCliffVariation from './variations'; import TerrainModel from './terrainmodel'; import randomStandSequence from './standsequence'; import Unit from './unit'; import Doodad from './doodad'; import TerrainDoodad from './terraindoodad'; import groundVert from './shaders/ground.vert'; import groundFrag from './shaders/ground.frag'; import waterVert from './shaders/water.vert'; import waterFrag from './shaders/water.frag'; import cliffsVert from './shaders/cliffs.vert'; import cliffsFrag from './shaders/cliffs.frag'; const normalHeap1 = vec3.create(); const normalHeap2 = vec3.create(); export default class War3MapViewer extends ModelViewer { wc3PathSolver: PathSolver; solverParams: { tileset?: string, reforged?: boolean, hd?: boolean } = {}; groundShader: ShaderProgram | null; waterShader: ShaderProgram | null; cliffShader: ShaderProgram | null; worldScene: Scene; waterIndex: number = 0; waterIncreasePerFrame: number = 0; waterHeightOffset: number = 0; waterTextures: Texture[] = []; maxDeepColor: Float32Array = new Float32Array(4); minDeepColor: Float32Array = new Float32Array(4); maxShallowColor: Float32Array = new Float32Array(4); minShallowColor: Float32Array = new Float32Array(4); anyReady: boolean = false; terrainCliffsAndWaterLoaded: boolean = false; terrainData: MappedData = new MappedData(); cliffTypesData: MappedData = new MappedData(); waterData: MappedData = new MappedData(); terrainReady: boolean = false; cliffsReady: boolean = false; doodadsAndDestructiblesLoaded: boolean = false; doodadsData: MappedData = new MappedData(); doodadMetaData: MappedData = new MappedData(); destructableMetaData: MappedData = new MappedData(); doodads: Doodad[] = []; terrainDoodads: any[] = []; doodadsReady: boolean = false; unitsAndItemsLoaded: boolean = false; unitsData: MappedData = new MappedData(); unitMetaData: MappedData = new MappedData(); units: Unit[] = []; unitsReady: boolean = false; tilesetTextures: Texture[] = []; cliffTextures: Texture[] = []; cliffModels: TerrainModel[] = []; mapMpq: War3Map | null = null; mapPathSolver: PathSolver | null = null; corners: Corner[][] = []; centerOffset: Float32Array = new Float32Array(2); mapSize: Int32Array = new Int32Array(2); tilesets: any[] = []; blightTextureIndex: number = -1; cliffTilesets: any[] = []; columns: number = 0; rows: number = 0; vertexBuffer: WebGLBuffer | null = null; faceBuffer: WebGLBuffer | null = null; instanceBuffer: WebGLBuffer | null = null; textureBuffer: WebGLBuffer | null = null; variationBuffer: WebGLBuffer | null = null; waterBuffer: WebGLBuffer | null = null; heightMap: WebGLTexture | null = null; waterHeightMap: WebGLTexture | null = null; cliffHeightMap: WebGLTexture | null = null; constructor(canvas: HTMLCanvasElement, wc3PathSolver: PathSolver) { super(canvas); let webgl = this.webgl; // Data textures. if (!webgl.ensureExtension('OES_texture_float')) { throw new Error('War3MapViewer: No float texture support!'); } // Optionally used for cliff renering. if (!webgl.ensureExtension('OES_vertex_array_object')) { console.warn('War3MapViewer: No vertex array object support! This might reduce performance.'); } this.on('error', (target, error, reason) => console.error(target, error, reason)); this.addHandler(mdxHandler); this.wc3PathSolver = wc3PathSolver; this.groundShader = this.webgl.createShaderProgram(groundVert, groundFrag); this.waterShader = this.webgl.createShaderProgram(waterVert, waterFrag); this.cliffShader = this.webgl.createShaderProgram(cliffsVert, cliffsFrag); this.worldScene = this.addScene(); let terrain = this.loadMapGeneric('TerrainArt\\Terrain.slk', 'text'); let cliffTypes = this.loadMapGeneric('TerrainArt\\CliffTypes.slk', 'text'); let water = this.loadMapGeneric('TerrainArt\\Water.slk', 'text'); this.whenLoaded([terrain, cliffTypes, water], () => { this.terrainCliffsAndWaterLoaded = true; this.terrainData.load(terrain.data); this.cliffTypesData.load(cliffTypes.data); this.waterData.load(water.data); this.emit('terrainloaded'); }) let doodads = this.loadMapGeneric('Doodads\\Doodads.slk', 'text'); let doodadMetaData = this.loadMapGeneric('Doodads\\DoodadMetaData.slk', 'text'); let destructableData = this.loadMapGeneric('Units\\DestructableData.slk', 'text'); let destructableMetaData = this.loadMapGeneric('Units\\DestructableMetaData.slk', 'text'); this.whenLoaded([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'); }); let unitData = this.loadMapGeneric('Units\\UnitData.slk', 'text'); let unitUi = this.loadMapGeneric('Units\\unitUI.slk', 'text'); let itemData = this.loadMapGeneric('Units\\ItemData.slk', 'text'); let unitMetaData = this.loadMapGeneric('Units\\UnitMetaData.slk', 'text'); this.whenLoaded([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'); }); } load(src: any) { return super.load(src, <PathSolver>this.mapPathSolver, this.solverParams); } loadMapGeneric(path: string, dataType: FetchDataTypeName) { return super.loadGeneric(urlWithParams(this.wc3PathSolver(path)[0], this.solverParams), dataType); } /** * Load a map from a buffer. */ async loadMap(buffer: ArrayBuffer) { let promise = this.promise(); // Readonly mode to optimize memory usage. let mapMpq = new War3Map(buffer, true); this.mapMpq = mapMpq; let wc3PathSolver = this.wc3PathSolver; let tileset = 'A'; let w3iFile = mapMpq.get('war3map.w3i'); if (w3iFile) { let w3iBuffer = w3iFile.arrayBuffer(); if (w3iBuffer) { let w3i = new War3MapW3i(w3iBuffer); tileset = w3i.tileset; } else { console.warn(`Attempted to load war3map.w3i but it couldn't be decoded. Using default tileset A.`); } } else { console.warn(`Attempted to load war3map.w3i but it's not there. Using default tileset A.`); } this.emit('maploaded'); this.solverParams.tileset = tileset.toLowerCase(); this.mapPathSolver = (path, params) => { // MPQ paths have backwards slashes...always? Don't know. let mpqPath = path.replace(/\//g, '\\'); // If the file is in the map, return it. let file = mapMpq.get(mpqPath); if (file) { return [file.arrayBuffer(), path.substr(path.lastIndexOf('.')), false]; } // Try to get the file from the server. return wc3PathSolver(path, params); }; let w3eFile = mapMpq.get('war3map.w3e'); if (w3eFile) { let w3eBuffer = w3eFile.arrayBuffer(); if (w3eBuffer) { let w3e = new War3MapW3e(w3eBuffer); let centerOffset = w3e.centerOffset; let mapSize = w3e.mapSize; this.corners = w3e.corners; this.centerOffset.set(centerOffset); this.mapSize.set(mapSize); // Override the grid based on the map. this.worldScene.grid = new Grid(centerOffset[0], centerOffset[1], mapSize[0] * 128 - 128, mapSize[1] * 128 - 128, 16 * 128, 16 * 128); this.emit('tilesetloaded'); if (this.terrainCliffsAndWaterLoaded) { this.loadTerrainCliffsAndWater(w3e); } else { this.once('terrainloaded', () => this.loadTerrainCliffsAndWater(w3e)); } } else { console.warn(`Attempted to load war3map.w3e but it couldn't be decoded. Terrain won't be rendered.`); } } else { console.warn(`Attempted to load war3map.w3e but it's not there. Terrain won't be rendered.`); } 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)); } promise.resolve(); } async loadTerrainCliffsAndWater(w3e: War3MapW3e) { let texturesExt = this.solverParams.reforged ? '.dds' : '.blp'; let tileset = w3e.tileset; for (let groundTileset of w3e.groundTilesets) { let row = this.terrainData.getRow(groundTileset); this.tilesets.push(row); this.tilesetTextures.push(<Texture>this.load(`${row.dir}\\${row.file}${texturesExt}`)); } 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(<Texture>this.load(`TerrainArt\\Blight\\${blights[tileset]}_Blight${texturesExt}`)); for (let cliffTileset of w3e.cliffTilesets) { let row = this.cliffTypesData.getRow(cliffTileset); this.cliffTilesets.push(row); this.cliffTextures.push(<Texture>this.load(`${row.texDir}\\${row.texFile}${texturesExt}`)); } let waterRow = this.waterData.getRow(`${tileset}Sha`); this.waterHeightOffset = <number>waterRow.height; this.waterIncreasePerFrame = <number>waterRow.texRate / 60; this.waterTextures.length = 0; this.maxDeepColor.set([<number>waterRow.Dmax_R, <number>waterRow.Dmax_G, <number>waterRow.Dmax_B, <number>waterRow.Dmax_A]); this.minDeepColor.set([<number>waterRow.Dmin_R, <number>waterRow.Dmin_G, <number>waterRow.Dmin_B, <number>waterRow.Dmin_A]); this.maxShallowColor.set([<number>waterRow.Smax_R, <number>waterRow.Smax_G, <number>waterRow.Smax_B, <number>waterRow.Smax_A]); this.minShallowColor.set([<number>waterRow.Smin_R, <number>waterRow.Smin_G, <number>waterRow.Smin_B, <number>waterRow.Smin_A]); for (let i = 0, l = waterRow.numTex; i < l; i++) { this.waterTextures.push(<Texture>this.load(`${waterRow.texFile}${i < 10 ? '0' : ''}${i}${texturesExt}`)); } await this.whenLoaded(this.tilesetTextures); let gl = this.gl; let corners = w3e.corners; let [columns, rows] = <Int32Array>this.mapSize; let centerOffset = <Float32Array>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: { [key: string]: { locations: number[], textures: number[] } } = {}; 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(); let texture = textures[0]; cornerTextures[instance * 4] = texture + 1; cornerVariations[instance * 4] = this.getVariation(texture, bottomLeft.groundVariation); textures.shift(); for (let i = 0, l = textures.length; i < l; i++) { let bitset = 0; texture = textures[i]; 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; } } } this.vertexBuffer = this.gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW); this.faceBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.faceBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint8Array([0, 1, 2, 1, 3, 2]), gl.STATIC_DRAW); this.cliffHeightMap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.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); this.heightMap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.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); this.waterHeightMap = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.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); this.instanceBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(instanceCount).map((currentValue, index, array) => index), gl.STATIC_DRAW); this.textureBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer); gl.bufferData(gl.ARRAY_BUFFER, cornerTextures, gl.STATIC_DRAW); this.variationBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.variationBuffer); gl.bufferData(gl.ARRAY_BUFFER, cornerVariations, gl.STATIC_DRAW); this.waterBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.waterBuffer); gl.bufferData(gl.ARRAY_BUFFER, waterFlags, gl.STATIC_DRAW); this.terrainReady = true; this.anyReady = true; let cliffShader = <ShaderProgram>this.cliffShader; let cliffPromises = Object.entries(cliffs).map(async (cliff) => { let path = cliff[0]; let { locations, textures } = cliff[1]; let resource = this.loadMapGeneric(path, 'arrayBuffer'); await resource.whenLoaded(); return new TerrainModel(this, resource.data, locations, textures, cliffShader); }); this.cliffModels = await Promise.all(cliffPromises); this.cliffsReady = true; } loadDoodadsAndDestructibles(modifications: any) { let mpq = <War3Map>this.mapMpq; let dooFile = mpq.get('war3map.doo'); if (!dooFile) { console.warn(`Attempted to load war3map.doo but it's not there`); return; } let dooBuffer = dooFile.arrayBuffer(); if (!dooBuffer) { console.warn(`Attempted to load war3map.doo but it couldn't be decoded`); return; } this.applyModificationFile(this.doodadsData, this.doodadMetaData, modifications.w3d); this.applyModificationFile(this.doodadsData, this.destructableMetaData, modifications.w3b); let doo = new War3MapDoo(dooBuffer); // Doodads and destructibles. for (let doodad of doo.doodads) { let row = this.doodadsData.getRow(doodad.id); let file = <string>row.file; let numVar = <number>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 = mpq.get(fileVar) || mpq.get(file); let model; if (mpqFile) { model = <MdxModel>this.load(mpqFile.name); } else { model = <MdxModel>this.load(fileVar); } this.doodads.push(new Doodad(this, model, row, doodad)) } // Cliff/Terrain doodads. for (let doodad of doo.terrainDoodads) { let row = this.doodadsData.getRow(doodad.id); let model = <MdxModel>this.load(`${row.file}.mdx`); this.terrainDoodads.push(new TerrainDoodad(this, model, row, doodad)); // let pathTexture = <Texture>this.load(row.pathTex); // pathTexture.whenLoaded(() => { // let startx = doodad.location[0]; // let starty = doodad.location[1]; // let endx = startx + pathTexture.width / 4; // let endy = starty + pathTexture.height / 4; // for (let x = startx; x < endx; x++) { // for (let y = starty; y < endy; y++) { // } // } // }); } this.doodadsReady = true; this.anyReady = true; } loadUnitsAndItems(modifications: any) { let mpq = <War3Map>this.mapMpq; let dooFile = mpq.get('war3mapUnits.doo'); if (!dooFile) { console.warn(`Attempted to load war3mapUnits.doo but it's not there`); return; } let dooBuffer = dooFile.arrayBuffer(); if (!dooBuffer) { console.warn(`Attempted to load war3mapUnits.doo but it couldn't be decoded`); return; } this.applyModificationFile(this.unitsData, this.unitMetaData, modifications.w3u); this.applyModificationFile(this.unitsData, this.unitMetaData, modifications.w3t); let unitsDoo = new War3MapUnitsDoo(dooBuffer); // Collect the units and items data. for (let unit of unitsDoo.units) { let row; let path; // Hardcoded? if (unit.id === 'sloc') { path = 'Objects\\StartLocation\\StartLocation.mdx'; } else { row = this.unitsData.getRow(unit.id); path = <string>row.file; if (path.endsWith('.mdl')) { path = path.slice(0, -4); } path += '.mdx'; } if (path) { let model = <MdxModel>this.load(path); this.units.push(new Unit(this, model, row, unit)); } else { console.log('Unknown unit ID', unit.id, unit); } } this.unitsReady = true; this.anyReady = true; } /** * Update the map. */ update() { if (this.anyReady) { this.waterIndex += this.waterIncreasePerFrame; if (this.waterIndex >= this.waterTextures.length) { this.waterIndex = 0; } super.update(); let instances = <MdxModelInstance[]>this.worldScene.instances; for (let instance of instances) { if (instance.sequenceEnded || instance.sequence === -1) { randomStandSequence(instance); } } } } /** * Render the map. */ render() { if (this.anyReady) { let worldScene = this.worldScene; worldScene.startFrame(); this.renderGround(); this.renderCliffs(); worldScene.renderOpaque(); this.renderWater(); worldScene.renderTranslucent(); } } renderGround() { if (this.terrainReady) { let gl = this.gl; let webgl = this.webgl; let instancedArrays = <ANGLE_instanced_arrays>webgl.extensions.ANGLE_instanced_arrays let shader = <ShaderProgram>this.groundShader; let uniforms = shader.uniforms; let attribs = shader.attribs; let tilesetTextures = this.tilesetTextures; let instanceAttrib = attribs.a_InstanceID; let positionAttrib = attribs.a_position; let texturesAttrib = attribs.a_textures; let variationsAttrib = attribs.a_variations; let tilesetCount = tilesetTextures.length; // This includes the blight texture. gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); webgl.useShaderProgram(shader); gl.uniformMatrix4fv(uniforms.u_VP, false, this.worldScene.camera.viewProjectionMatrix); gl.uniform2fv(uniforms.u_offset, <Float32Array>this.centerOffset); gl.uniform2f(uniforms.u_size, this.columns, this.rows); gl.uniform1i(uniforms.u_heightMap, 15); gl.activeTexture(gl.TEXTURE15); gl.bindTexture(gl.TEXTURE_2D, this.heightMap); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 0, 0); gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer); gl.vertexAttribPointer(instanceAttrib, 1, gl.FLOAT, false, 0, 0); instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 1); gl.bindBuffer(gl.ARRAY_BUFFER, this.textureBuffer); gl.vertexAttribPointer(texturesAttrib, 4, gl.UNSIGNED_BYTE, false, 0, 0); instancedArrays.vertexAttribDivisorANGLE(texturesAttrib, 1); gl.bindBuffer(gl.ARRAY_BUFFER, this.variationBuffer); gl.vertexAttribPointer(variationsAttrib, 4, gl.UNSIGNED_BYTE, false, 0, 0); instancedArrays.vertexAttribDivisorANGLE(variationsAttrib, 1); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.faceBuffer); gl.uniform1f(uniforms.u_baseTileset, 0); for (let i = 0, l = Math.min(tilesetCount, 15); i < l; i++) { let isExtended = tilesetTextures[i].width > tilesetTextures[i].height ? 1 : 0; gl.uniform1f(uniforms[`u_extended[${i}]`], isExtended); gl.uniform1i(uniforms[`u_tilesets[${i}]`], i); tilesetTextures[i].bind(i); } instancedArrays.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this.rows * this.columns); if (tilesetCount > 15) { gl.uniform1f(uniforms.u_baseTileset, 15); for (let i = 0, l = tilesetCount - 15; i < l; i++) { let isExtended = tilesetTextures[i + 15].width > tilesetTextures[i + 15].height ? 1 : 0; gl.uniform1f(uniforms[`u_extended[${i}]`], isExtended); tilesetTextures[i + 15].bind(i); } instancedArrays.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this.rows * this.columns); } 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 = <ANGLE_instanced_arrays>webgl.extensions.ANGLE_instanced_arrays let shader = <ShaderProgram>this.waterShader; let uniforms = shader.uniforms; let attribs = shader.attribs; let instanceAttrib = attribs.a_InstanceID; let positionAttrib = attribs.a_position; let isWaterAttrib = attribs.a_isWater; gl.depthMask(false); gl.enable(gl.BLEND); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); webgl.useShaderProgram(shader); gl.uniformMatrix4fv(uniforms.u_VP, false, this.worldScene.camera.viewProjectionMatrix); gl.uniform2fv(uniforms.u_offset, <Float32Array>this.centerOffset); gl.uniform2f(uniforms.u_size, this.columns, this.rows); gl.uniform1i(uniforms.u_heightMap, 0); gl.uniform1i(uniforms.u_waterHeightMap, 1); gl.uniform1i(uniforms.u_waterTexture, 2); gl.uniform1f(uniforms.u_offsetHeight, this.waterHeightOffset); 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, this.heightMap); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, this.waterHeightMap); this.waterTextures[this.waterIndex | 0].bind(2); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.vertexAttribPointer(positionAttrib, 2, gl.FLOAT, false, 8, 0); gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceBuffer); gl.vertexAttribPointer(instanceAttrib, 1, gl.FLOAT, false, 4, 0); instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 1); gl.bindBuffer(gl.ARRAY_BUFFER, this.waterBuffer); gl.vertexAttribPointer(isWaterAttrib, 1, gl.UNSIGNED_BYTE, false, 1, 0); instancedArrays.vertexAttribDivisorANGLE(isWaterAttrib, 1); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.faceBuffer); instancedArrays.drawElementsInstancedANGLE(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this.rows * this.columns); instancedArrays.vertexAttribDivisorANGLE(isWaterAttrib, 0); instancedArrays.vertexAttribDivisorANGLE(instanceAttrib, 0); } } renderCliffs() { if (this.cliffsReady) { let gl = this.gl; let webgl = this.webgl; let instancedArrays = <ANGLE_instanced_arrays>webgl.extensions.ANGLE_instanced_arrays let vertexArrayObject = <OES_vertex_array_object>webgl.extensions.OES_vertex_array_object; let shader = <ShaderProgram>this.cliffShader; let attribs = shader.attribs; let uniforms = shader.uniforms; gl.disable(gl.BLEND); shader.use(); gl.uniformMatrix4fv(uniforms.u_VP, false, this.worldScene.camera.viewProjectionMatrix); gl.uniform1i(uniforms.u_heightMap, 0); gl.uniform2f(uniforms.u_pixel, 1 / (this.columns + 1), 1 / (this.rows + 1)); gl.uniform2fv(uniforms.u_centerOffset, <Float32Array>this.centerOffset); gl.uniform1i(uniforms.u_texture1, 1); gl.uniform1i(uniforms.u_texture2, 2); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, this.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 (!vertexArrayObject) { instancedArrays.vertexAttribDivisorANGLE(attribs.a_instancePosition, 1); instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceTexture, 1); } // Render the cliffs. for (let cliff of this.cliffModels) { cliff.render(shader); } // Clear instanced attributes. if (!vertexArrayObject) { instancedArrays.vertexAttribDivisorANGLE(attribs.a_instancePosition, 0); instancedArrays.vertexAttribDivisorANGLE(attribs.a_instanceTexture, 0); } } } cliffFileName(bottomLeftLayer: number, bottomRightLayer: number, topLeftLayer: number, topRightLayer: number, base: number) { return String.fromCharCode(65 + bottomLeftLayer - base) + String.fromCharCode(65 + topLeftLayer - base) + String.fromCharCode(65 + topRightLayer - base) + String.fromCharCode(65 + bottomRightLayer - base); } getVariation(groundTexture: number, variation: number) { 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? */ isCliff(column: number, row: number) { 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? */ isWater(column: number, row: number) { 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. */ cliffGroundIndex(whichCliff: number) { 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. */ cornerTexture(column: number, row: number) { 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; } applyModificationFile(dataMap: MappedData, metadataMap: MappedData, modificationFile: any) { if (modificationFile) { // Modifications to built-in objects this.applyModificationTable(dataMap, metadataMap, modificationFile.originalTable); // Declarations of user-defined objects this.applyModificationTable(dataMap, metadataMap, modificationFile.customTable); } } applyModificationTable(dataMap: MappedData, metadataMap: MappedData, modificationTable: any) { 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[<string>metadata.field] = modification.value; } else { console.warn('Unknown modification ID', modification); } } } } groundNormal(out: vec3, x: number, y: number) { let centerOffset = <Float32Array>this.centerOffset; let mapSize = <Int32Array>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; } */