Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
Document,
|
||||
NodeIO,
|
||||
} from "@gltf-transform/core";
|
||||
import {
|
||||
ALL_EXTENSIONS,
|
||||
KHRMaterialsVariants,
|
||||
KHRTextureTransform,
|
||||
} from "@gltf-transform/extensions";
|
||||
|
||||
const REPEAT = 10497;
|
||||
const CLAMP_TO_EDGE = 33071;
|
||||
|
||||
function addAccessor(document, buffer, name, type, array) {
|
||||
return document.createAccessor(name, buffer).setType(type).setArray(array);
|
||||
}
|
||||
|
||||
function createTerrainMaterial(document, textureTransformExtension, name, texture, tileScale, extras = null) {
|
||||
const material = document.createMaterial(name)
|
||||
.setBaseColorTexture(texture)
|
||||
.setBaseColorFactor([1, 1, 1, 1])
|
||||
.setMetallicFactor(0)
|
||||
.setRoughnessFactor(1)
|
||||
.setAlphaMode("OPAQUE");
|
||||
const textureInfo = material.getBaseColorTextureInfo();
|
||||
textureInfo.setWrapS(REPEAT).setWrapT(REPEAT);
|
||||
textureInfo.setExtension(KHRTextureTransform.EXTENSION_NAME, textureTransformExtension.createTransform()
|
||||
.setScale([Number(tileScale), -Number(tileScale)]));
|
||||
if (extras) material.setExtras(extras);
|
||||
return material;
|
||||
}
|
||||
|
||||
function addVariantMapping(document, variantExtension, mappingList, material, name) {
|
||||
const variant = variantExtension.createVariant(name);
|
||||
mappingList.addMapping(variantExtension.createMapping().setMaterial(material).addVariant(variant));
|
||||
return variant;
|
||||
}
|
||||
|
||||
function setTextureProvenance(texture, sourceDDS, extra = {}) {
|
||||
texture.setExtras({
|
||||
...texture.getExtras(),
|
||||
runewakerSourceDDS: sourceDDS,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
|
||||
function createVisualDocument(converted, textureSources, provenance) {
|
||||
const slug = provenance.dungeonId ?? "sand-vortex";
|
||||
const title = provenance.dungeonTitle ?? "Sand Vortex";
|
||||
const document = new Document();
|
||||
const root = document.getRoot();
|
||||
const buffer = document.createBuffer(`${slug}-native-terrain-buffer`);
|
||||
const scene = document.createScene(`${title} native terrain`);
|
||||
const partitionChunks = provenance.partitionVisualChunks === true;
|
||||
const mesh = partitionChunks ? null : document.createMesh(`${title} terrain tiles`);
|
||||
const chunkNodes = [];
|
||||
const variants = document.createExtension(KHRMaterialsVariants).setRequired(false);
|
||||
const textureTransform = document.createExtension(KHRTextureTransform).setRequired(false);
|
||||
const textureBySource = new Map();
|
||||
for (const [sourceKey, details] of textureSources.textureBySource) {
|
||||
const texture = document.createTexture(details.imageName)
|
||||
.setImage(details.image)
|
||||
.setMimeType("image/png")
|
||||
.setExtras({
|
||||
runewakerSourceDDS: details.sourceDDS,
|
||||
...(details.sourceCompressedFile ? { runewakerSourceCompressed: details.sourceCompressedFile } : {}),
|
||||
...(details.channelLayerIds ? { runewakerWeightChannels: details.channelLayerIds } : {}),
|
||||
runewakerColorPolicy: details.alphaPolicy ?? details.colorSpace,
|
||||
});
|
||||
textureBySource.set(sourceKey, texture);
|
||||
}
|
||||
const positions = new Float32Array(converted.vertexCount * 3);
|
||||
const normals = new Float32Array(converted.vertexCount * 3);
|
||||
const uvs = new Float32Array(converted.vertexCount * 2);
|
||||
|
||||
for (const chunk of converted.chunks) {
|
||||
positions.set(chunk.positions, chunk.vertexOffset * 3);
|
||||
normals.set(chunk.normals, chunk.vertexOffset * 3);
|
||||
uvs.set(chunk.uvs, chunk.vertexOffset * 2);
|
||||
}
|
||||
|
||||
const positionAccessor = partitionChunks ? null : addAccessor(document, buffer, `${slug}-native-terrain-positions`, "VEC3", positions);
|
||||
const normalAccessor = partitionChunks ? null : addAccessor(document, buffer, `${slug}-native-terrain-normals`, "VEC3", normals);
|
||||
const uvAccessor = partitionChunks ? null : addAccessor(document, buffer, `${slug}-native-terrain-normalized-local-xz`, "VEC2", uvs);
|
||||
|
||||
for (const chunk of converted.chunks) {
|
||||
const layers = chunk.layers;
|
||||
const weights = chunk.splatMaps;
|
||||
if (!Array.isArray(layers) || layers.length < 1 || layers.length > 6) {
|
||||
throw new Error(`${chunk.id} must contain 1–6 source terrain layers.`);
|
||||
}
|
||||
if (!Array.isArray(weights) || weights.length !== Math.ceil(layers.length / 3)) {
|
||||
throw new Error(`${chunk.id} weight-map count does not match its terrain-layer count.`);
|
||||
}
|
||||
const metadata = {
|
||||
version: 1,
|
||||
layerVariants: chunk.material.variantNames,
|
||||
weightVariants: chunk.material.weightVariantNames,
|
||||
tileScales: layers.map((layer) => Number(layer.tileScale)),
|
||||
...(provenance.cliffProjection ? { cliffProjection: provenance.cliffProjection } : {}),
|
||||
};
|
||||
if (metadata.layerVariants.length !== layers.length || metadata.weightVariants.length !== weights.length) {
|
||||
throw new Error(`${chunk.id} variant name counts do not match its source terrain data.`);
|
||||
}
|
||||
|
||||
const firstLayer = layers[0];
|
||||
const firstTexture = textureBySource.get(firstLayer.sourceDDS);
|
||||
if (!firstTexture) throw new Error(`${chunk.id} default tile image is missing: ${firstLayer.sourceDDS}`);
|
||||
const defaultMaterial = createTerrainMaterial(
|
||||
document,
|
||||
textureTransform,
|
||||
`${chunk.id} default layer ${firstLayer.layerId}`,
|
||||
firstTexture,
|
||||
firstLayer.tileScale,
|
||||
{ runewakerTerrain: metadata },
|
||||
);
|
||||
const mappingList = variants.createMappingList();
|
||||
addVariantMapping(document, variants, mappingList, defaultMaterial, metadata.layerVariants[0]);
|
||||
|
||||
for (let layerIndex = 1; layerIndex < layers.length; layerIndex += 1) {
|
||||
const layer = layers[layerIndex];
|
||||
const texture = textureBySource.get(layer.sourceDDS);
|
||||
if (!texture) throw new Error(`${chunk.id} layer image is missing: ${layer.sourceDDS}`);
|
||||
const material = createTerrainMaterial(
|
||||
document,
|
||||
textureTransform,
|
||||
`${chunk.id} layer ${layer.layerId}`,
|
||||
texture,
|
||||
layer.tileScale,
|
||||
);
|
||||
addVariantMapping(document, variants, mappingList, material, metadata.layerVariants[layerIndex]);
|
||||
}
|
||||
|
||||
for (let weightIndex = 0; weightIndex < weights.length; weightIndex += 1) {
|
||||
const weight = weights[weightIndex];
|
||||
const texture = textureBySource.get(weight.linearPngFile);
|
||||
if (!texture) throw new Error(`${chunk.id} linear splat image is missing: ${weight.linearPngFile}`);
|
||||
const material = document.createMaterial(`${chunk.id} linear weights ${weight.mapIndex}`)
|
||||
.setMetallicFactor(0)
|
||||
.setRoughnessFactor(1)
|
||||
.setAlphaMode("OPAQUE")
|
||||
.setMetallicRoughnessTexture(texture)
|
||||
.setOcclusionTexture(texture)
|
||||
.setExtras({
|
||||
runewakerTerrainWeightMap: {
|
||||
mapIndex: weight.mapIndex,
|
||||
channelLayerIds: weight.channelLayerIds,
|
||||
sourceDDS: weight.splatDDS,
|
||||
linearPngFile: weight.linearPngFile,
|
||||
channels: "linear RGB; source alpha ignored",
|
||||
},
|
||||
});
|
||||
material.getMetallicRoughnessTextureInfo().setWrapS(CLAMP_TO_EDGE).setWrapT(CLAMP_TO_EDGE);
|
||||
material.getOcclusionTextureInfo().setWrapS(CLAMP_TO_EDGE).setWrapT(CLAMP_TO_EDGE);
|
||||
addVariantMapping(document, variants, mappingList, material, metadata.weightVariants[weightIndex]);
|
||||
}
|
||||
|
||||
const indices = addAccessor(
|
||||
document,
|
||||
buffer,
|
||||
`${chunk.id}-source-triangle-indices-with-mask-slots`,
|
||||
"SCALAR",
|
||||
partitionChunks ? Uint16Array.from(chunk.visualIndices, index => index - chunk.vertexOffset) : chunk.visualIndices,
|
||||
);
|
||||
const primitive = document.createPrimitive()
|
||||
.setAttribute("POSITION", positionAccessor ?? addAccessor(document, buffer, `${chunk.id}-positions`, "VEC3", chunk.positions))
|
||||
.setAttribute("NORMAL", normalAccessor ?? addAccessor(document, buffer, `${chunk.id}-normals`, "VEC3", chunk.normals))
|
||||
.setAttribute("TEXCOORD_0", uvAccessor ?? addAccessor(document, buffer, `${chunk.id}-uvs`, "VEC2", chunk.uvs))
|
||||
.setIndices(indices)
|
||||
.setMaterial(defaultMaterial)
|
||||
.setExtension(KHRMaterialsVariants.EXTENSION_NAME, mappingList)
|
||||
.setExtras({
|
||||
runewakerTerrainChunk: {
|
||||
id: chunk.id,
|
||||
coordinate: chunk.coordinate,
|
||||
sourceVertexFlagsSha256: chunk.vertexFlagsSha256 ?? null,
|
||||
sourceCellVisibilityFlagsSha256: chunk.cellVisibilityFlagsSha256 ?? null,
|
||||
sourceTriangleVisibilityFlags: Buffer.from(chunk.triangleVisibilityFlags).toString("base64"),
|
||||
sourceTriangleVisibilityFlagsEncoding: "base64 encoded Uint8Array; one value per source triangle; 1 visible / 0 masked",
|
||||
sourceTriangleVisibilitySha256: chunk.triangleVisibilitySha256 ?? null,
|
||||
sourceTriangleSlots: chunk.triangleVisibilityFlags.length,
|
||||
visibleSourceTriangles: chunk.visibleSourceTriangles,
|
||||
maskedSourceTriangles: chunk.maskedSourceTriangles,
|
||||
},
|
||||
});
|
||||
if (mesh) mesh.addPrimitive(primitive);
|
||||
else chunkNodes.push(document.createNode(chunk.id)
|
||||
.setMesh(document.createMesh(`${chunk.id} native tile`).addPrimitive(primitive)));
|
||||
}
|
||||
|
||||
const node = document.createNode(`${title} native terrain`)
|
||||
.setExtras({
|
||||
runewakerTerrain: {
|
||||
version: 1,
|
||||
source: "native WDB terrain chunks; no resampling or layer baking",
|
||||
chunkCount: converted.chunks.length,
|
||||
partition: partitionChunks ? "native-chunk-meshes" : "shared-grid-mesh",
|
||||
sourceManifestSha256: provenance.sourceManifestSha256,
|
||||
primaryPlacementGuid: provenance.primaryPlacementGuid,
|
||||
},
|
||||
});
|
||||
if (mesh) node.setMesh(mesh);
|
||||
for (const chunkNode of chunkNodes) node.addChild(chunkNode);
|
||||
scene.addChild(node);
|
||||
root.setExtras({
|
||||
runewakerTerrain: {
|
||||
version: 1,
|
||||
source: "Runewaker native WDB heightfield and texture list",
|
||||
sourceManifestSha256: provenance.sourceManifestSha256,
|
||||
sourceWdbSha256: provenance.sourceWdbSha256,
|
||||
primaryPlacementGuid: provenance.primaryPlacementGuid,
|
||||
metersPerSourceUnit: provenance.metersPerSourceUnit,
|
||||
coordinateBridge: "inverse primary descriptor TRS, then (x,y,z)=(x,y,-z)*metersPerSourceUnit",
|
||||
vertexNormalSource: "decoded native vertexData RGB; native terrain shader normal",
|
||||
textureSourceDDs: provenance.textureSourceDDS,
|
||||
},
|
||||
});
|
||||
root.setDefaultScene(scene);
|
||||
return document;
|
||||
}
|
||||
|
||||
function createCollisionDocument(converted, provenance) {
|
||||
const slug = provenance.dungeonId ?? "sand-vortex";
|
||||
const title = provenance.dungeonTitle ?? "Sand Vortex";
|
||||
const document = new Document();
|
||||
const root = document.getRoot();
|
||||
const buffer = document.createBuffer(`${slug}-native-terrain-collision-buffer`);
|
||||
const scene = document.createScene(`${title} native terrain collision`);
|
||||
const mesh = document.createMesh(`${title} native terrain collision surfaces`);
|
||||
const partitionChunks = provenance.partitionVisualChunks === true;
|
||||
const positions = new Float32Array(partitionChunks ? 0 : converted.vertexCount * 3);
|
||||
if (!partitionChunks) for (const chunk of converted.chunks) positions.set(chunk.positions, chunk.vertexOffset * 3);
|
||||
const positionAccessor = partitionChunks ? null : addAccessor(document, buffer, `${slug}-native-terrain-collision-positions`, "VEC3", positions);
|
||||
|
||||
for (const chunk of converted.chunks) {
|
||||
const indices = addAccessor(
|
||||
document,
|
||||
buffer,
|
||||
`${chunk.id}-visible-collision-triangles`,
|
||||
"SCALAR",
|
||||
partitionChunks ? Uint16Array.from(chunk.collisionIndices, index => index - chunk.vertexOffset) : chunk.collisionIndices,
|
||||
);
|
||||
mesh.addPrimitive(document.createPrimitive()
|
||||
.setAttribute("POSITION", positionAccessor ?? addAccessor(document, buffer, `${chunk.id}-collision-positions`, "VEC3", chunk.positions))
|
||||
.setIndices(indices)
|
||||
.setExtras({
|
||||
runewakerTerrainCollisionChunk: {
|
||||
id: chunk.id,
|
||||
coordinate: chunk.coordinate,
|
||||
sourceTriangleSlots: chunk.triangleVisibilityFlags.length,
|
||||
sourceVisibleTriangles: chunk.visibleSourceTriangles,
|
||||
includedCollisionTriangles: chunk.collisionIndices.length / 3,
|
||||
maskedTrianglesExcluded: chunk.maskedSourceTriangles,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
scene.addChild(document.createNode(`${title} native terrain collision`).setMesh(mesh));
|
||||
root.setExtras({
|
||||
runewakerTerrainCollision: {
|
||||
version: 1,
|
||||
sourceManifestSha256: provenance.sourceManifestSha256,
|
||||
sourceTriangleSlots: converted.report.sourceTriangleSlots,
|
||||
includedVisibleTriangles: converted.report.collisionTriangles,
|
||||
maskedOrDegenerateTrianglesExcluded: converted.report.excludedCollisionTriangles,
|
||||
},
|
||||
});
|
||||
root.setDefaultScene(scene);
|
||||
return document;
|
||||
}
|
||||
|
||||
export async function writeNativeTerrainGlbs({ converted, textureSources, provenance, visualFile, collisionFile }) {
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
await io.write(visualFile, createVisualDocument(converted, textureSources, provenance));
|
||||
await io.write(collisionFile, createCollisionDocument(converted, provenance));
|
||||
}
|
||||
Reference in new issue
Block a user