101 lines
5.7 KiB
TypeScript
101 lines
5.7 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { Group, Mesh, MeshStandardMaterial, NoColorSpace, PlaneGeometry, RepeatWrapping, SRGBColorSpace, Texture } from "three";
|
|
import type { GLTF, GLTFParser } from "three-stdlib";
|
|
import { bindWowTerrainMaterial, createWowTerrainPlugin } from "./wowTerrainMaterial";
|
|
import { prepareDungeonMaterial } from "./productionDungeonMaterials";
|
|
import { applyDungeonMeshGraphics } from "./graphicsMaterialQuality";
|
|
import { disposeObject3DResources, materialTextures } from "./threeResourceDisposal";
|
|
|
|
const roles = { cutoutPatterns: [], cutoutNames: [], additivePatterns: [], additiveNames: [], blendPatterns: [], blendNames: [] };
|
|
const metadata = {
|
|
schemaVersion: 1, sourceAdt: { path: "world/maps/tanarisinstance/tanarisinstance_30_29.adt", sha256: "a".repeat(64) },
|
|
layerVariants: ["dirt", "rock"], weightVariants: ["chunk-alpha"],
|
|
uv: { attribute: "TEXCOORD_0", basis: "mcnk-local-normalized", diffuseScale: [8, 8], weightScale: [1, 1], vSign: 1 },
|
|
composition: { rule: "wow-overlay-residual", normalize: false },
|
|
};
|
|
|
|
function fixture() {
|
|
const layers = [new Texture(), new Texture()];
|
|
layers.forEach(texture => {
|
|
texture.colorSpace = SRGBColorSpace;
|
|
texture.wrapS = RepeatWrapping;
|
|
texture.wrapT = RepeatWrapping;
|
|
});
|
|
const weights = new Texture();
|
|
const material = new MeshStandardMaterial({ map: layers[0] });
|
|
material.userData.wowTerrain = structuredClone(metadata);
|
|
const scene = new Group();
|
|
scene.add(new Mesh(new PlaneGeometry(), material));
|
|
const carriers = [new MeshStandardMaterial({ map: layers[0] }), new MeshStandardMaterial({ map: layers[1] }),
|
|
new MeshStandardMaterial({ aoMap: weights })];
|
|
const parser: Pick<GLTFParser, "json" | "getDependency"> = {
|
|
json: { extensions: { KHR_materials_variants: { variants: ["dirt", "rock", "chunk-alpha"].map(name => ({ name })) } },
|
|
meshes: [{ primitives: [{ extensions: { KHR_materials_variants: {
|
|
mappings: carriers.map((_, index) => ({ material: index, variants: [index] })),
|
|
} } }] }] },
|
|
getDependency: vi.fn(async (_kind, index) => carriers[index]),
|
|
};
|
|
return { layers, weights, material, scene, carriers, parser };
|
|
}
|
|
|
|
describe("source-backed WoW terrain materials", () => {
|
|
it("keeps native tile detail and linear blend data through production cloning, filtering and disposal", async () => {
|
|
const { layers, weights, material, scene, parser } = fixture();
|
|
await createWowTerrainPlugin(parser).afterRoot!({ scene } as GLTF);
|
|
const clone = prepareDungeonMaterial(material, roles, "adt-hybrid");
|
|
const allTextures = [...layers, weights];
|
|
expect(new Set(materialTextures(clone))).toEqual(new Set(allTextures));
|
|
expect(layers.every(texture => texture.colorSpace === SRGBColorSpace)).toBe(true);
|
|
expect(weights.colorSpace).toBe(NoColorSpace);
|
|
expect(clone.customProgramCacheKey()).toBe(material.customProgramCacheKey());
|
|
const root = new Group();
|
|
const geometry = new PlaneGeometry();
|
|
root.add(new Mesh(geometry, clone), new Mesh(geometry, clone));
|
|
applyDungeonMeshGraphics(root, "environment", "high", 16, 8);
|
|
expect(allTextures.every(texture => texture.anisotropy === 8)).toBe(true);
|
|
const disposals = allTextures.map(texture => vi.spyOn(texture, "dispose"));
|
|
expect(disposeObject3DResources(root)).toEqual({ geometries: 1, materials: 1, textures: 3, skeletons: 0 });
|
|
disposals.forEach(dispose => expect(dispose).toHaveBeenCalledOnce());
|
|
});
|
|
|
|
it("rejects unverified sources and unsupported normalized/reversed blends before loading carriers", async () => {
|
|
for (const change of [
|
|
(data: typeof metadata) => { data.sourceAdt.sha256 = "unknown"; },
|
|
(data: typeof metadata) => { data.composition.normalize = true; },
|
|
(data: typeof metadata) => { data.uv.vSign = -1; },
|
|
]) {
|
|
const { material, scene, parser } = fixture();
|
|
change(material.userData.wowTerrain);
|
|
await expect(createWowTerrainPlugin(parser).afterRoot!({ scene } as GLTF)).rejects.toThrow("Unsupported or unverified");
|
|
expect(parser.getDependency).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it("requires the linear AO carrier for native weights and refuses missing or ambiguous variants", async () => {
|
|
const first = fixture();
|
|
first.carriers[2].aoMap = null;
|
|
first.carriers[2].map = first.weights;
|
|
await expect(createWowTerrainPlugin(first.parser).afterRoot!({ scene: first.scene } as GLTF)).rejects.toThrow("Missing native WoW terrain image");
|
|
const second = fixture();
|
|
second.parser.json.meshes[0].primitives[0].extensions.KHR_materials_variants.mappings.pop();
|
|
await expect(createWowTerrainPlugin(second.parser).afterRoot!({ scene: second.scene } as GLTF)).rejects.toThrow("Missing native WoW terrain carrier");
|
|
const third = fixture();
|
|
third.parser.json.meshes[0].primitives[0].extensions.KHR_materials_variants.mappings.push({ material: 1, variants: [0] });
|
|
await expect(createWowTerrainPlugin(third.parser).afterRoot!({ scene: third.scene } as GLTF)).rejects.toThrow("ambiguous native WoW");
|
|
});
|
|
|
|
it("supports base-only native chunks and refuses an incomplete multilayer binding", () => {
|
|
const material = new MeshStandardMaterial();
|
|
const base = new Texture();
|
|
base.colorSpace = SRGBColorSpace;
|
|
base.wrapS = RepeatWrapping;
|
|
base.wrapT = RepeatWrapping;
|
|
bindWowTerrainMaterial(material, { layers: [base] });
|
|
expect(materialTextures(material)).toEqual([base]);
|
|
expect(() => bindWowTerrainMaterial(material, { layers: [base, new Texture()] })).toThrow("layer layout");
|
|
const invalidWeights = new Texture();
|
|
invalidWeights.colorSpace = SRGBColorSpace;
|
|
expect(() => bindWowTerrainMaterial(material, { layers: [base, base], weights: invalidWeights })).toThrow("clamped linear blend");
|
|
});
|
|
});
|