Files
Battle-for-Tismo/tests/textures_test.js

230 lines
10 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { MapTopology, key, mapToLocal } from "../shared/hex.js";
import { GLMapRenderer } from "../client/js/map_view/webgl.js";
import {
TEXTURE_DESIRED_REPEAT,
blendBeach,
blendTextures,
isTexturedLand,
landTexture,
overlayNormal,
terrainLayers,
textureRepeat,
vertexTextureWeights,
} from "../client/js/map_view/textures.js";
// A flat fixture with the given terrain per coordinate, plus the renderer
// helpers `_textureRing`/`_textureWeights` under test.
function fixture(terrainAt, size = 5) {
const topology = new MapTopology({ x: size, y: size }, false);
const tiles = {};
for (let y = topology.originY; y < topology.originY + size; y++) {
for (let x = topology.originX; x < topology.originX + size; x++) {
tiles[key(x, y)] = { terrainType: terrainAt(x, y), col: 0 };
}
}
const renderer = Object.create(GLMapRenderer.prototype);
return { renderer, topology, tiles, view: { seed: 7, topology, tiles, _period: 0 } };
}
export class TexturesTest extends TestCase {
test_grassy_land_uses_grass_and_desert_uses_sand() {
for (const type of ["Forest", "Hills", "Land", "Mountain", "Tundra"]) {
this.assertEqual(landTexture(type).base, "grass", `${type} base`);
}
this.assertEqual(landTexture("Desert").base, "sand", "desert base");
this.assertNull(landTexture("Sea"));
this.assertNull(landTexture("Ice"));
}
test_only_terrain_with_a_tiling_texture_is_blended() {
this.assertTrue(isTexturedLand({ terrainType: "Land" }));
this.assertTrue(isTexturedLand({ terrainType: "Desert" }));
this.assertTrue(isTexturedLand({ terrainType: "Mountain" }));
this.assertTrue(isTexturedLand({ terrainType: "Tundra" }));
this.assertFalse(isTexturedLand({ terrainType: "Sea" }));
this.assertFalse(isTexturedLand(null));
}
test_forest_and_mountains_overlay_the_base() {
this.assertEqual(terrainLayers("Forest"), [
"assets/textures/forest.png",
"assets/textures/grass.png",
]);
this.assertEqual(terrainLayers("Hills"), ["assets/textures/grass.png"]);
this.assertEqual(terrainLayers("Mountain"), [
"assets/textures/mountain.png",
"assets/textures/grass.png",
]);
this.assertEqual(terrainLayers("Tundra"), [
"assets/textures/mountain.png",
"assets/textures/grass.png",
]);
this.assertEqual(terrainLayers("Desert"), ["assets/textures/sand.png"]);
this.assertEqual(terrainLayers("Land"), ["assets/textures/grass.png"]);
this.assertEmpty(terrainLayers("Ice"), "the atlas terrains keep their art");
}
test_only_mountains_have_a_normal_map() {
this.assertEqual(overlayNormal("mountain"), "mountains_normal_map");
this.assertNull(overlayNormal("hills"), "hills have no overlay any more");
this.assertNull(overlayNormal("forest"));
this.assertNull(overlayNormal("sand"));
}
test_the_repeat_snaps_to_a_whole_number_across_the_wrap() {
const repeat = textureRepeat(1890);
this.assertApprox(Math.round(1890 / repeat), 1890 / repeat, 1e-9, "a whole number of repeats");
this.assertApprox(repeat, TEXTURE_DESIRED_REPEAT, 4, "close to the desired density");
this.assertEqual(textureRepeat(0), TEXTURE_DESIRED_REPEAT, "flat maps use the desired repeat");
}
test_a_shared_edge_blends_to_an_even_mix() {
// (0,0) is land and (1,0) is desert; the point sits on their shared edge,
// halfway between the two centres.
const blend = blendTextures([
{ tile: { terrainType: "Land" }, dx: -23.625, dy: -14 },
{ tile: { terrainType: "Desert" }, dx: 23.625, dy: 14 },
]);
this.assertApprox(blend.grass, 0.5, 1e-9);
this.assertApprox(blend.sand, 0.5, 1e-9);
}
test_a_tile_centre_stays_almost_purely_its_own_texture() {
const blend = blendTextures([
{ tile: { terrainType: "Desert" }, dx: 0, dy: 0 },
{ tile: { terrainType: "Land" }, dx: 47.25, dy: 28 },
{ tile: { terrainType: "Land" }, dx: 47.25, dy: -28 },
{ tile: { terrainType: "Land" }, dx: 0, dy: 56 },
]);
this.assertGreater(blend.sand, 0.9, "the tile centre keeps its sand");
this.assertLess(blend.grass, 0.1, "the neighbours barely bleed in");
}
test_overlays_fade_towards_grass() {
const centre = blendTextures([
{ tile: { terrainType: "Forest" }, dx: 0, dy: 0 },
{ tile: { terrainType: "Land" }, dx: 47.25, dy: 28 },
{ tile: { terrainType: "Land" }, dx: 0, dy: 56 },
]);
this.assertGreater(centre.forest, 0.9, "a forest tile keeps its overlay");
const edge = blendTextures([
{ tile: { terrainType: "Forest" }, dx: -23.625, dy: -14 },
{ tile: { terrainType: "Land" }, dx: 23.625, dy: 14 },
]);
this.assertApprox(edge.forest, 0.5, 1e-9, "the overlay is half strength on the shared edge");
const hills = blendTextures([{ tile: { terrainType: "Hills" }, dx: 0, dy: 0 }]);
this.assertApprox(hills.grass, 1, 1e-9, "hills sit on grass");
this.assertApprox(hills.forest, 0, 1e-9, "hills have no overlay of their own");
this.assertApprox(hills.mountain, 0, 1e-9, "hills have no mountain overlay");
const mountain = blendTextures([{ tile: { terrainType: "Mountain" }, dx: 0, dy: 0 }]);
this.assertApprox(mountain.mountain, 1, 1e-9, "a mountain keeps its overlay");
this.assertApprox(mountain.grass, 1, 1e-9, "mountains sit on grass");
const tundra = blendTextures([{ tile: { terrainType: "Tundra" }, dx: 0, dy: 0 }]);
this.assertApprox(tundra.flat, 1, 1e-9, "tundra rides in the flat rock channel");
this.assertApprox(tundra.mountain, 0, 1e-9, "tundra does not need the height cap");
this.assertApprox(tundra.grass, 1, 1e-9, "tundra sits on grass");
}
test_the_blend_is_a_shared_field_from_either_tile() {
// Both tiles on a shared edge must compute the same weights, otherwise the
// transition would show a seam where the two fans meet.
const { renderer, tiles, view } = fixture((x) => (x >= 1 ? "Desert" : "Land"));
const centreLand = mapToLocal(0, 0);
const centreDesert = mapToLocal(1, 0);
const ringLand = renderer._textureRing(view, 0, 0, centreLand);
const ringDesert = renderer._textureRing(view, 1, 0, centreDesert);
const px = (centreLand.x + centreDesert.x) / 2;
const py = (centreLand.y + centreDesert.y) / 2;
const fromLand = renderer._textureWeights(tiles[key(0, 0)], ringLand, px, py);
const fromDesert = renderer._textureWeights(tiles[key(1, 0)], ringDesert, px, py);
this.assertEqual(fromLand, fromDesert, "the shared vertex agrees");
this.assertApprox(fromLand[0], 0.5, 1e-9, "grass");
this.assertApprox(fromLand[1], 0.5, 1e-9, "sand");
}
test_terrain_vertices_carry_world_space_texture_coordinates() {
const { renderer, view } = fixture(() => "Land", 3);
renderer.terrain = { name: "terrain" };
renderer.gl = { STATIC_DRAW: 0, DYNAMIC_DRAW: 0 };
let captured = null;
renderer._upload = (pass, arr, usage, floats = 8) => {
captured = arr;
pass.floats = floats;
};
renderer.buildTerrain(view);
this.assertEqual(captured.length % 17, 0, "seventeen floats per terrain vertex");
// The undistorted world position rides along as UVs; tile (0,0)'s right
// corner is the ideal lattice point (31.5, 0), independent of the warp.
let found = false;
for (let i = 0; i < captured.length; i += 17) {
this.assertTrue(Number.isFinite(captured[i + 4]), "world x is finite");
this.assertTrue(Number.isFinite(captured[i + 5]), "world y is finite");
if (Math.abs(captured[i + 4] - 31.5) < 1e-9 && Math.abs(captured[i + 5]) < 1e-9) {
found = true;
}
}
this.assertTrue(found, "the ideal shared corner appears as a world coordinate");
}
test_atlas_terrain_ignores_the_land_blend() {
const atlas = vertexTextureWeights(
{ terrainType: "Ice" },
[{ tile: { terrainType: "Land" }, dx: 0, dy: 0 }]
);
this.assertEqual(atlas, [0, 0, 1, 0, 0, 0, 0, 0], "ice stays on the atlas");
const land = vertexTextureWeights(
{ terrainType: "Land" },
[{ tile: { terrainType: "Land" }, dx: 0, dy: 0 }]
);
this.assertEqual(land, [1, 0, 0, 0, 0, 0, 0, 0], "plains use the grass texture");
const mountain = vertexTextureWeights(
{ terrainType: "Mountain" },
[{ tile: { terrainType: "Mountain" }, dx: 0, dy: 0 }]
);
this.assertEqual(mountain, [1, 0, 0, 0, 1, 0, 0, 0], "mountains sit on grass with their overlay");
const tundra = vertexTextureWeights(
{ terrainType: "Tundra" },
[{ tile: { terrainType: "Tundra" }, dx: 0, dy: 0 }]
);
this.assertEqual(tundra, [1, 0, 0, 0, 0, 0, 0, 1], "tundra uses the flat rock channel");
}
test_sea_is_flagged_for_the_water_shader() {
const sea = vertexTextureWeights(
{ terrainType: "Sea" },
[{ tile: { terrainType: "Sea" }, dx: 0, dy: 0 }]
);
this.assertEqual(sea, [0, 0, 1, 0, 0, 1, 0, 0], "sea rides on the atlas but is flagged as water");
this.assertFalse(isTexturedLand({ terrainType: "Sea" }), "sea is not a tiling land texture");
}
test_a_coast_blends_sand_into_non_mountain_land() {
const samples = [
{ tile: { terrainType: "Land" }, dx: -23.625, dy: -14 },
{ tile: { terrainType: "Sea" }, dx: 23.625, dy: 14 },
];
const plains = vertexTextureWeights({ terrainType: "Land" }, samples);
this.assertGreater(plains[6], 0, "the shoreline gets some sand");
this.assertLess(plains[6], 1, "the ground still shows through");
const mountain = vertexTextureWeights({ terrainType: "Mountain" }, samples);
this.assertEqual(mountain[6], 0, "mountain coasts stay rocky");
const tundra = vertexTextureWeights({ terrainType: "Tundra" }, samples);
this.assertEqual(tundra[6], 0, "tundra coasts stay rocky");
}
test_beach_is_a_shared_field_between_land_and_sea() {
const inland = blendBeach([
{ tile: { terrainType: "Land" }, dx: 0, dy: 0 },
{ tile: { terrainType: "Land" }, dx: 47.25, dy: 28 },
]);
this.assertEqual(inland, 0, "inland ground has no beach");
const shore = blendBeach([
{ tile: { terrainType: "Land" }, dx: -23.625, dy: -14 },
{ tile: { terrainType: "Sea" }, dx: 23.625, dy: 14 },
]);
this.assertApprox(shore, 0.5, 1e-9, "the shoreline is an even mix");
}
}