Compare commits

...
3 Commits
Author SHA1 Message Date
phenom 147c2804d4 Release Healer Man 0.1.26 2026-09-09 22:18:15 -04:00
phenom e7cc588bfc Release Healer Man 0.1.25 2026-09-09 16:48:50 -04:00
phenom bac0da9300 Release Healer Man 0.1.24 2026-09-09 16:43:37 -04:00
138 changed files with 1873 additions and 328 deletions

No files matched your search

+153
View File
@@ -1839,3 +1839,156 @@ Vite's existing large-chunk advisory remains informational.
no environment-performance regressions. The release-candidate check accepted
all 118 changed paths. The existing large-chunk advisory and Rapier
initialization deprecation warning remain informational.
# 2026-09-09 — fixed per-rank classic mana costs
- Imported WoW mana percentages are now converted into fixed costs at each
spell rank's source level. Player level, Intellect, equipment, and maximum-mana
modifiers no longer increase the cost of an already learned rank; legitimate
talent and aura cost reductions are still applied afterward.
- Shadow Word: Pain rank 4 now costs 133 mana at every level from 26 through 33,
including level 32. Learning rank 5 at level 34 raises the cost to 188 mana.
- AI companions now use real character-scale mana pools so fixed spell costs do
not prevent healing or resurrection. The companion display also resolves the
active spell rank and the same final affordability result as the primary HUD.
- Verification passed all 1,133 Vitest cases across 208 files. The production
build completed with 974 modules transformed; arena, KTX2, and environment-
performance audits remained green, and the release-candidate check passed.
# 2026-09-09 — dungeon wall and nameplate performance regressions
- The 0.1.24 material fallback honored BLEND tags on legacy WoW WMO walls,
floors, and ceilings, disabling their depth writes. WoW environments now use
their authored material roles again; RuneWaker environments retain their
source MASK/BLEND semantics. Shipped Wailing Caverns wall materials, vines,
and water are covered by regression tests.
- The 0.1.25 boss-name occlusion scanned the complete rendered scene with
Three.js raycasts. A live Kalin's Shrine CPU profile was dominated by triangle
intersection and vertex decoding. Boss names and nearby NPC prompts now use
bounded camera-to-label queries against Rapier's existing physical spatial
index, at most ten times per second, skipping offscreen labels. Walls, floors,
and ceilings occlude labels; actors and trigger sensors do not.
- In an isolated offline Edge playtest at 1440 x 900 on an AMD RX 9070 XT,
Kalin's Shrine improved from about 10 FPS (98.5 ms/frame, 167 draw calls,
DPR 1.125) to 55.5-56.1 FPS (17.8-18.0 ms/frame, 168-170 draw calls,
DPR 1.5). This is a representative entrance/combat comparison, not an audit
of all thirty RoM dungeons. The environment alone took about 0.8 ms CPU and
0.5 ms GPU with 14 draw calls.
- Wailing Caverns screenshots show continuous opaque cavern walls and ceilings.
A controlled toggle of the old material flags reproduced the missing wall
sections at the same camera position; restoring depth-writing materials fixed
them. Its full scene ran at about 57 FPS. No uncaught client errors appeared.
- Evidence and repeatable local benchmark helpers are under
`playtest-artifacts/2026-09-09-dungeon-rendering/`: `kalin-before.cpuprofile`,
`kalin-after.cpuprofile`, `kalin-after-metrics.json`, `kalin-before.png`,
`kalin-after.png`, `wailing-regression.png`, and `wailing-restored.png`.
- All 26 focused material, label occlusion, combat sight, aggro visibility,
instancing, and NPC-prompt tests passed. Physical-occlusion tests exercise
actual Rapier worlds rather than just source-code assertions.
# 2026-09-09 — campaign rendering audit and Thor performance preparation
- Audited the actual visual GLB headers for all 54 WoW campaign dungeons:
9,402 materials, including 3,586 legacy BLEND materials restored to opaque
rendering by the shared policy. Every WoW package contained affected
materials. A permanent shipped-asset test covers all 54 definitions and
retains explicit cutout, blend, and additive roles. This is material-policy
coverage, not a claim that every room has been visually traversed.
- Live screenshots checked Wailing Caverns, Shadowfang Keep, Utgarde Pinnacle,
the Nexus, and Tinkertech Showdown. The sampled walls, floors, stairs, and
ceilings remain solid; authored lighting effects remain transparent. Kalin's
Shrine and Forsaken Abbey provided RoM rendering checks. Roster and in-dungeon
screenshots show character bodies and equipped weapon silhouettes after the
shared character animation change.
- A controlled Utgarde Pinnacle entrance test found about 1,739 draw calls,
1.95 million triangles, and 30.5 FPS on the desktop GPU. Clipping at the
existing full-fog distance reduced this to about 209 calls, 567,000 triangles,
and 60 FPS at the same viewpoint. The camera now follows each dungeon's fog
distance, including transitions that retain the existing Canvas/camera.
The background uses the fog color to avoid a clipping seam at the horizon.
All 84 dungeon definitions have render-distance coverage.
- Static dungeon visual transforms are cached until the root or its parent's
world transform changes. Tests verify imported matrices, repeated unchanged
frames, root movement, and parent movement. Characters, moving world objects,
and physics remain outside this immutable visual cache.
The final 63-second Utgarde sample held 60 FPS with 213 draw calls:
main-thread work averaged 9.2 ms/frame, renderer CPU averaged 7.5 ms
(p95 8.3 ms), and GPU averaged 2.7 ms (p95 4.1 ms). Movement and jump input
continued to animate the player while the environment remained stationary.
- Character locomotion and jump clip selections are cached by animation
library and weapon stance. The final Kalin CPU profile contains no sampled
per-frame base-clip selection/name parsing. Frame-cap deadlines now tolerate
sub-millisecond timestamp rounding without drifting or emitting catch-up
frames; tests cover 60 FPS on rounded 60 Hz and 120 Hz callbacks.
- A 31.5-second Kalin sample with a connected browser companion display held
60 FPS: main-thread work averaged 8.7 ms/frame, renderer CPU averaged 5.4 ms
(p95 5.9 ms), and GPU averaged 2.4 ms (p95 3.4 ms). The companion used about
5.1 ms of main-thread time per second. Recent ability-cost snapshot work was
not a dominant hotspot in this sample. Startup warmup is bounded to loading;
companion snapshots are already limited to ten per second during gameplay.
- These are isolated offline Edge desktop samples on an AMD RX 9070 XT at
1920 x 1080 CSS pixels, DPR 1.5 (2880 x 1620 renderbuffer). Party composition
can change between dungeon entries. They do not certify Android performance
or sustained busy-combat headroom. ADB listed no connected Android device.
- Thor acceptance should use the actual model, both native displays, target
resolution, and a sustained 20-minute traversal/combat run after warmup.
Provisional headroom target: p95 main-thread and GPU work each at or below
12 ms, with p99 presented frame intervals within the 16.7 ms 60-FPS budget.
Test large pulls, boss effects, menus, and repeated dungeon transitions;
record thermal behavior and frame-time spikes, not only average FPS.
The environment asset audit still has 60 grandfathered budget exceptions;
its green result means no new asset regression, not a Thor FPS guarantee.
- Evidence is in `playtest-artifacts/2026-09-09-thor-audit/`, including roster,
dungeon, and companion screenshots, CPU profiles, and CPU/GPU JSON samples.
The full material report is
`playtest-artifacts/2026-09-09-dungeon-rendering/wow-material-audit.json`.
- Validation passed 227 focused tests covering avatar rendering and animation,
camera distances, material policies, frame pacing, static transforms, label
visibility, and companion snapshots. The final static-cache change also
passed the 105-test scene subset. The production build and release-candidate
check passed; the existing large-bundle advisory remains.
# 2026-09-09 — additional CPU headroom from culling and shared skeletons
- Prepared dungeon visuals now group static siblings into conservative spatial
bounds. One scene hook checks these bounds after the current camera and world
matrices update, before Three.js traverses individual meshes. Bounds include
complete wall geometry and all instances; lights, animated meshes, and meshes
explicitly disabling frustum culling remain outside these groups. The hook
supports streamed chunks and restores its predecessor after cleanup.
- At the same Utgarde Pinnacle entrance, only 16 of 1,383 spatial groups were
visible. The renderer examined 108 static meshes instead of 7,002, retaining
the same 206 draw calls and 566,913 rendered triangles. Controlled culling
toggles produced byte-identical canvas PNGs across eight camera headings.
- In the same running scene, spatial grouping reduced average main-thread work
from 9.45 to 6.83 ms/frame, and renderer CPU from 7.81 to 5.31 ms/frame.
Removing containers emptied by static instancing reduced the final fresh-run
averages further to 6.49 ms main-thread and 4.85 ms renderer CPU (p95 5.2 ms).
The final sample lasted 87.8 seconds and held 60 FPS. Authored empty scene
nodes are preserved; only containers emptied by our batching are removed.
- The shared character renderer still uses SkeletonUtils.clone, then reuses
equivalent skeleton palettes within that owned clone. Bone identities, order,
and inverse bind matrices must all match; per-mesh bind transforms remain
unchanged. Different characters retain independent bones and animations.
Owned bone textures are now explicitly disposed during character teardown.
- A live character audit found 66 skeleton objects for one player palette and
256 objects for four party palettes. A Wailing Caverns toggle removed 309
duplicate skeletons and produced an identical rendered image. Tests verify
posed vertices, differing bind transforms, incompatible palettes, independent
cloned characters, and redundant texture disposal.
In the same Wailing scene, main-thread work fell from 7.40 to 6.70 ms/frame;
renderer CPU fell from 5.51 to 4.86 ms/frame, and allocated GPU textures fell
from 292 to 217. This measures palette sharing after spatial culling was
already enabled. Both samples held 60 FPS.
- The final built runtime showed a complete roster body and staff with one
skeleton for 66 skinned meshes. Kalin's Shrine showed one player palette and
four independent party palettes; walking retained the player body and weapon
silhouette. Wailing Caverns and Utgarde screenshots retained solid dungeon
surfaces. There were no uncaught client errors during these playtests.
- All 210 targeted avatar, visibility, instancing, material, camera-distance,
and label tests passed. The production build and release-candidate check
passed. The existing large-bundle advisory remains.
- Evidence is under `playtest-artifacts/2026-09-09-static-culling/`: paired PNGs,
eight-heading pixel comparisons, profiles, before/after timing JSON, and
screenshots. Measurements use the same desktop Edge/RX 9070 XT setup as the
preceding audit. These CPU improvements do not certify Thor performance.
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "healer-man",
"version": "0.1.23",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "healer-man",
"version": "0.1.23",
"version": "0.1.26",
"dependencies": {
"@capacitor/android": "8.4.1",
"@capacitor/core": "8.4.1",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "healer-man",
"private": true,
"version": "0.1.23",
"version": "0.1.26",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0",
@@ -95,6 +95,7 @@
"runewaker:audit:encounters": "node scripts/runewaker-pipeline/audit-encounter-implementation.mjs && node --test scripts/runewaker-pipeline/encounter-implementation-report.test.mjs",
"runewaker:audit:environment-assembly": "node scripts/runewaker-pipeline/audit-environment-assembly.mjs && node --test scripts/runewaker-pipeline/environment-assembly.test.mjs",
"runewaker:audit:navigation": "node scripts/runewaker-pipeline/analyze-navigation-gaps.mjs && vitest run src/game/runewakerObjectiveRouteAudit.test.ts",
"runewaker:audit:material-alpha": "node scripts/runewaker-pipeline/audit-material-alpha.mjs",
"assets:wailing:instance": "gltf-transform instance ../wow335a/dungeon-export-work/WailingCaverns/converted/wailing-caverns.visual.glb src/assets/game/dungeons/wailing-caverns/wailing-caverns-visual.glb --min 3",
"assets:wailing:creatures:animated": "node scripts/prepareAnimatedCreatures.mjs",
"assets:wailing:creatures:combat": "node scripts/add-combat-creature-clips.mjs",
+2 -2
View File
@@ -1,4 +1,4 @@
{
"versionName": "0.1.23",
"versionCode": 1023
"versionName": "0.1.26",
"versionCode": 1026
}
@@ -36,6 +36,9 @@ async function main() {
&& hasBasisu
&& /^\/images\/\d+\/mimeType$/.test(issue.pointer ?? "")
&& issue.message.includes("'image/ktx2'")) return false;
if (issue.code === "IMAGE_UNRECOGNIZED_FORMAT"
&& hasBasisu
&& /^\/images\/\d+$/.test(issue.pointer ?? "")) return false;
return true;
});
rows.push({
@@ -0,0 +1,262 @@
import { createHash } from "node:crypto";
import { readFile, readdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import sharp from "sharp";
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const recipeDirectory = path.join(projectRoot, "scripts", "runewaker-pipeline", "recipes");
const workRoot = path.join(projectRoot, "runewaker-export-work");
const shippingRoot = path.join(projectRoot, "src", "assets", "game", "dungeons");
const resourceCacheRoot = path.join(workRoot, "resource-cache");
const requestedSlugs = new Set(process.argv.slice(2).filter((value) => !value.startsWith("--")));
const fix = process.argv.includes("--fix");
const alphaClassifications = new Map();
let resourceTextureIndex;
async function exists(file) {
try {
await stat(file);
return true;
} catch {
return false;
}
}
function parseGlb(buffer, file) {
if (buffer.length < 20 || buffer.toString("utf8", 0, 4) !== "glTF") {
throw new Error(`${file} is not a GLB file.`);
}
const chunks = [];
let json;
let offset = 12;
while (offset + 8 <= buffer.length) {
const chunkLength = buffer.readUInt32LE(offset);
const chunkType = buffer.readUInt32LE(offset + 4);
const chunkStart = offset + 8;
const chunkEnd = chunkStart + chunkLength;
if (chunkEnd > buffer.length) break;
const data = buffer.subarray(chunkStart, chunkEnd);
chunks.push({ type: chunkType, data });
if (chunkType === 0x4e4f534a) {
json = JSON.parse(data.toString("utf8").replace(/\0+$/u, "").trim());
}
offset = chunkEnd;
}
if (!json) throw new Error(`${file} does not contain a JSON chunk.`);
return { json, chunks };
}
function serializeGlb(source, json, chunks) {
const encodedJson = Buffer.from(JSON.stringify(json));
const paddedJson = Buffer.alloc(Math.ceil(encodedJson.length / 4) * 4, 0x20);
encodedJson.copy(paddedJson);
const updatedChunks = chunks.map((chunk) => (
chunk.type === 0x4e4f534a ? { ...chunk, data: paddedJson } : chunk
));
const totalLength = 12 + updatedChunks.reduce((total, chunk) => total + 8 + chunk.data.length, 0);
const output = Buffer.alloc(totalLength);
source.copy(output, 0, 0, 12);
output.writeUInt32LE(totalLength, 8);
let offset = 12;
for (const chunk of updatedChunks) {
output.writeUInt32LE(chunk.data.length, offset);
output.writeUInt32LE(chunk.type, offset + 4);
chunk.data.copy(output, offset + 8);
offset += 8 + chunk.data.length;
}
return output;
}
async function classifyAlpha(file) {
if (alphaClassifications.has(file)) return alphaClassifications.get(file);
const metadata = await sharp(file).metadata();
if (!metadata.hasAlpha) {
alphaClassifications.set(file, "opaque");
return "opaque";
}
const { data, info } = await sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
let hasTransparentPixel = false;
for (let offset = 3; offset < data.length; offset += info.channels) {
const alpha = data[offset];
if (alpha === 255) continue;
hasTransparentPixel = true;
if (alpha > 0) {
alphaClassifications.set(file, "blend");
return "blend";
}
}
const role = hasTransparentPixel ? "cutout" : "opaque";
alphaClassifications.set(file, role);
return role;
}
function sourceImageIndex(texture) {
return texture?.extensions?.KHR_texture_basisu?.source ?? texture?.source;
}
async function indexResourceTextures() {
if (resourceTextureIndex) return resourceTextureIndex;
resourceTextureIndex = new Map();
if (!(await exists(resourceCacheRoot))) return resourceTextureIndex;
const cacheDirectories = (await readdir(resourceCacheRoot, { withFileTypes: true }))
.filter((entry) => entry.isDirectory())
.sort((left, right) => left.name.localeCompare(right.name));
for (const cacheDirectory of cacheDirectories) {
const textureDirectory = path.join(resourceCacheRoot, cacheDirectory.name, "prepared", "textures");
if (!(await exists(textureDirectory))) continue;
for (const texture of (await readdir(textureDirectory)).filter((file) => file.endsWith(".png")).sort()) {
if (!resourceTextureIndex.has(texture)) {
resourceTextureIndex.set(texture, path.join(textureDirectory, texture));
}
}
}
return resourceTextureIndex;
}
async function resolveSourceTexture(localDirectory, imageName) {
const fileName = `${imageName}.png`;
const localFile = path.join(localDirectory, fileName);
if (await exists(localFile)) return localFile;
return (await indexResourceTextures()).get(fileName);
}
function sha256(buffer) {
return createHash("sha256").update(buffer).digest("hex");
}
async function refreshShippingMetadata(metadataFile, metadata, visualArtifact, visualBuffer) {
visualArtifact.size = visualBuffer.length;
visualArtifact.sha256 = sha256(visualBuffer);
await writeFile(metadataFile, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
const optimizationFile = path.join(path.dirname(metadataFile), "optimization-report.json");
if (!(await exists(optimizationFile))) return;
const optimization = JSON.parse(await readFile(optimizationFile, "utf8"));
if (optimization.optimized) {
optimization.optimized.size = visualBuffer.length;
if (Object.hasOwn(optimization.optimized, "checksum")) {
optimization.optimized.checksum = sha256(visualBuffer);
}
}
await writeFile(optimizationFile, `${JSON.stringify(optimization, null, 2)}\n`, "utf8");
}
async function auditDungeon(recipe) {
const slug = recipe.slug;
const metadataFile = path.join(shippingRoot, slug, "import-metadata.json");
const preparedTextureDirectory = path.join(workRoot, slug, "prepared", "textures");
if (!(await exists(metadataFile))) {
return { slug, status: "incomplete", problems: [`Missing ${path.relative(projectRoot, metadataFile)}`] };
}
if (!(await exists(preparedTextureDirectory))) {
return {
slug,
status: "incomplete",
problems: [`Missing ${path.relative(projectRoot, preparedTextureDirectory)}`],
};
}
const metadata = JSON.parse(await readFile(metadataFile, "utf8"));
const visualArtifact = (metadata.assets ?? metadata.artifacts ?? [])
.find((artifact) => artifact.role === "visual");
const visualFile = path.join(shippingRoot, slug, visualArtifact?.fileName ?? `${slug}-visual.glb`);
if (!(await exists(visualFile))) {
return { slug, status: "incomplete", problems: [`Missing ${path.relative(projectRoot, visualFile)}`] };
}
const sourceGlb = await readFile(visualFile);
const { json: gltf, chunks } = parseGlb(sourceGlb, visualFile);
const materialResults = [];
const missingTextures = [];
for (const [materialIndex, material] of (gltf.materials ?? []).entries()) {
const textureIndex = material.pbrMetallicRoughness?.baseColorTexture?.index;
const texture = gltf.textures?.[textureIndex];
const image = gltf.images?.[sourceImageIndex(texture)];
if (!image?.name) continue;
const sourceFile = await resolveSourceTexture(preparedTextureDirectory, image.name);
if (!sourceFile) {
missingTextures.push(`${image.name}.png`);
continue;
}
const expectedRole = await classifyAlpha(sourceFile);
const expectedAlphaMode = expectedRole === "blend" ? "BLEND" : expectedRole === "cutout" ? "MASK" : "OPAQUE";
const actualAlphaMode = material.alphaMode ?? "OPAQUE";
const actualRole = material.extras?.semanticRole ?? "missing";
materialResults.push({
materialIndex,
material: material.name ?? `material-${materialIndex}`,
texture: image.name,
expectedRole,
expectedAlphaMode,
actualRole,
actualAlphaMode,
matches: expectedAlphaMode === actualAlphaMode && expectedRole === actualRole,
});
if (fix && (expectedAlphaMode !== actualAlphaMode || expectedRole !== actualRole)) {
if (expectedAlphaMode === "OPAQUE") delete material.alphaMode;
else material.alphaMode = expectedAlphaMode;
if (expectedAlphaMode === "MASK") material.alphaCutoff = 0.5;
else delete material.alphaCutoff;
material.extras = { ...(material.extras ?? {}), semanticRole: expectedRole };
}
}
const mismatches = materialResults.filter((result) => !result.matches);
const alphaMaterials = materialResults.filter((result) => result.expectedRole !== "opaque");
const problems = [
...mismatches.map((result) =>
`${result.texture}: expected ${result.expectedAlphaMode}/${result.expectedRole}, got ${result.actualAlphaMode}/${result.actualRole}`,
),
...[...new Set(missingTextures)].map((file) => `Missing prepared source texture ${file}`),
];
if (fix && mismatches.length > 0 && missingTextures.length === 0) {
const visualBuffer = serializeGlb(sourceGlb, gltf, chunks);
await writeFile(visualFile, visualBuffer);
if (visualArtifact) {
await refreshShippingMetadata(metadataFile, metadata, visualArtifact, visualBuffer);
}
}
return {
slug,
status: problems.length === 0 ? "green" : fix && missingTextures.length === 0 ? "repaired" : "mismatch",
materials: materialResults.length,
alphaMaterials: alphaMaterials.map(({ material, texture, expectedAlphaMode }) => ({
material,
texture,
alphaMode: expectedAlphaMode,
})),
mismatches,
problems,
};
}
const recipeFiles = (await readdir(recipeDirectory))
.filter((file) => file.endsWith(".json") && !file.endsWith("-population.json"))
.sort();
const recipes = [];
for (const recipeFile of recipeFiles) {
const recipe = JSON.parse(await readFile(path.join(recipeDirectory, recipeFile), "utf8"));
if (!recipe.slug || recipe.slug === "runewaker-player-avatars") continue;
if (requestedSlugs.size > 0 && !requestedSlugs.has(recipe.slug)) continue;
recipes.push(recipe);
}
const results = [];
for (const recipe of recipes) {
results.push(await auditDungeon(recipe));
}
const failures = results.filter((result) => !["green", "repaired"].includes(result.status));
const report = {
schemaVersion: 1,
dungeonCount: results.length,
status: failures.length === 0 ? fix ? "repaired" : "green" : "mismatch",
failureCount: failures.length,
alphaDungeonCount: results.filter((result) => result.alphaMaterials?.length > 0).length,
results,
};
console.log(JSON.stringify(report, null, 2));
if (failures.length > 0) process.exitCode = 1;
@@ -6,6 +6,7 @@ Expected invocation:
"""
from __future__ import annotations
from array import array
import json
import math
import os
@@ -111,6 +112,69 @@ def material_image_key(material: bpy.types.Material) -> str:
return f"material:{material.name.casefold()}"
def material_image(material: bpy.types.Material) -> bpy.types.Image | None:
if not material.use_nodes or material.node_tree is None:
return None
for node in material.node_tree.nodes:
if node.type == "TEX_IMAGE" and node.image is not None:
return node.image
return None
def image_alpha_role(image: bpy.types.Image | None) -> str:
"""Classify source alpha without turning fully opaque RGBA textures translucent."""
if image is None or image.channels < 4:
return "opaque"
pixels = array("f", [0.0]) * len(image.pixels)
image.pixels.foreach_get(pixels)
alpha_values = pixels[3::image.channels]
if min(alpha_values, default=1.0) >= 0.999:
return "opaque"
if any(0.001 < alpha < 0.999 for alpha in alpha_values):
return "blend"
return "cutout"
def preserve_material_alpha(material: bpy.types.Material) -> str:
image = material_image(material)
role = image_alpha_role(image)
if role == "opaque" or material.node_tree is None:
return role
image_node = next(
node
for node in material.node_tree.nodes
if node.type == "TEX_IMAGE" and node.image == image
)
principled = next(
(node for node in material.node_tree.nodes if node.type == "BSDF_PRINCIPLED"),
None,
)
if principled is None:
return role
alpha_output = image_node.outputs.get("Alpha")
alpha_input = principled.inputs.get("Alpha")
if alpha_output is None or alpha_input is None:
return role
if role == "blend":
if not alpha_input.is_linked:
material.node_tree.links.new(alpha_output, alpha_input)
material.blend_method = "BLEND"
if hasattr(material, "surface_render_method"):
material.surface_render_method = "BLENDED"
else:
alpha_clip = material.node_tree.nodes.new("ShaderNodeMath")
alpha_clip.name = "RuneWaker Alpha Clip"
alpha_clip.operation = "GREATER_THAN"
alpha_clip.inputs[1].default_value = 0.5
material.node_tree.links.new(alpha_output, alpha_clip.inputs[0])
material.node_tree.links.new(alpha_clip.outputs[0], alpha_input)
material.blend_method = "CLIP"
material.alpha_threshold = 0.5
return role
def deduplicate_materials(objects: list[bpy.types.Object]) -> dict[str, int]:
canonical: dict[str, bpy.types.Material] = {}
replacements = 0
@@ -126,7 +190,7 @@ def deduplicate_materials(objects: list[bpy.types.Object]) -> dict[str, int]:
replacements += 1
else:
canonical[key] = material
material["semanticRole"] = "opaque"
material["semanticRole"] = preserve_material_alpha(material)
material["sourceFormat"] = "RuneWaker DDS"
return {"unique": len(canonical), "replacements": replacements}
@@ -241,6 +241,7 @@ async function cachedResourceAsset(resourceRecord) {
throw new Error("source ROS is missing: " + projectPath(sourceFile));
}
const sourceSha256 = await sha256(sourceFile);
const converterSha256 = await sha256(blenderScript);
const cacheRoot = path.join(resourceCacheDirectory, resourceRecord.cacheKey);
const raw = path.join(cacheRoot, "raw");
const prepared = path.join(cacheRoot, "prepared");
@@ -253,6 +254,7 @@ async function cachedResourceAsset(resourceRecord) {
.map((file) => path.join(cacheRoot, file));
if (cached.source?.resource === resourceRecord.normalizedResource
&& cached.source?.sha256 === sourceSha256
&& cached.toolchain?.converterSha256 === converterSha256
&& cached.coordinateSystem?.metersPerSourceUnit === recipe.coordinateSystem.metersPerSourceUnit
&& (await Promise.all(outputs.map(exists))).every(Boolean)) {
return {
@@ -300,6 +302,9 @@ async function cachedResourceAsset(resourceRecord) {
nativeBridge: recipe.coordinateSystem.nativeBridge,
metersPerSourceUnit: recipe.coordinateSystem.metersPerSourceUnit,
},
toolchain: {
converterSha256,
},
visual: {
file: path.relative(
cacheRoot,
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "aeternal-circle-visual.glb",
"size": 4963912,
"sha256": "42fdcff1b25b58688943a030f69fd14ea839247a7baabbc31b5f6f7faaf49785",
"size": 4963968,
"sha256": "de1ba06b7e3a9237fcff75aff2b00093287975e755cc7e98e77de1b7af6a9c14",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "aeternal-circle-visual.optimized.glb",
"size": 4963912,
"checksum": "42fdcff1b25b58688943a030f69fd14ea839247a7baabbc31b5f6f7faaf49785"
"size": 4963968,
"checksum": "de1ba06b7e3a9237fcff75aff2b00093287975e755cc7e98e77de1b7af6a9c14"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "barren-caves-visual.glb",
"size": 1710152,
"sha256": "d0f733de4b26c0f432c5e160614765f320bf1753a829be269740b340e7415a12",
"size": 1710188,
"sha256": "e6dc49e6aa6fd20cee1b2afa43324a714f8f8284a6becef8dc514509b432f9bb",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "barren-caves-visual.optimized.glb",
"size": 1710152,
"checksum": "d0f733de4b26c0f432c5e160614765f320bf1753a829be269740b340e7415a12"
"size": 1710188,
"checksum": "e6dc49e6aa6fd20cee1b2afa43324a714f8f8284a6becef8dc514509b432f9bb"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "cave-water-dragon-visual.glb",
"size": 8893112,
"sha256": "cc57c0285de77690c4a1cdabaecd8b192791cbd05aca38aa21ed96462688ca31",
"size": 8893204,
"sha256": "dd1887f88d6ee9102da0e34ea2b4087742e37a2b8ce779c722bc124effe1faca",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "cave-water-dragon-visual.optimized.glb",
"size": 8893112,
"checksum": "cc57c0285de77690c4a1cdabaecd8b192791cbd05aca38aa21ed96462688ca31"
"size": 8893204,
"checksum": "dd1887f88d6ee9102da0e34ea2b4087742e37a2b8ce779c722bc124effe1faca"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "demon-stronghold-125-visual.glb",
"size": 15134500,
"sha256": "ecbacfe274e7eecb104c5807eedf3cc51cd4b7a628967c6331fc93b86396b842",
"size": 15135060,
"sha256": "e2106115dcc216999d2e37d26eeb52844ce4f41b44e19594699f37a54d7f19b9",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "demon-stronghold-125-visual.optimized.glb",
"size": 15134500,
"checksum": "ecbacfe274e7eecb104c5807eedf3cc51cd4b7a628967c6331fc93b86396b842"
"size": 15135060,
"checksum": "e2106115dcc216999d2e37d26eeb52844ce4f41b44e19594699f37a54d7f19b9"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "demon-stronghold-visual.glb",
"size": 15166984,
"sha256": "9368984b3c183a8db6e8b7e18b7818edcf41b8edd45ffeca1d1df28aadc592a2",
"size": 15167544,
"sha256": "664181dc790b9e68cdc7451dbd0b1170a033a71ca19375743b6e66ca25889723",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "demon-stronghold-visual.optimized.glb",
"size": 15166984,
"checksum": "9368984b3c183a8db6e8b7e18b7818edcf41b8edd45ffeca1d1df28aadc592a2"
"size": 15167544,
"checksum": "664181dc790b9e68cdc7451dbd0b1170a033a71ca19375743b6e66ca25889723"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "dungeon-of-dalanis-visual.glb",
"size": 11936864,
"sha256": "4c584ef34779e516f8e6ff3a4907eaab1c7c8d49d4c302de66e17b09222d8829",
"size": 11937088,
"sha256": "45cad819bc348dec8926f7de237c625e9d18697b4200804e0440dd16b8976959",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "dungeon-of-dalanis-visual.optimized.glb",
"size": 11936864,
"checksum": "4c584ef34779e516f8e6ff3a4907eaab1c7c8d49d4c302de66e17b09222d8829"
"size": 11937088,
"checksum": "45cad819bc348dec8926f7de237c625e9d18697b4200804e0440dd16b8976959"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -284,8 +284,8 @@
{
"role": "visual",
"fileName": "forsaken-abbey-visual.glb",
"size": 9580212,
"sha256": "37800c26cdf784658a57dcc7328ab1b9b23e46d018be6d1e6420f127768b0332",
"size": 9580568,
"sha256": "ee7afd5e9179e6bcb7ec9171798466282e09d4ef696a3f9639bcd2bbc2ae8f48",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "forsaken-abbey-visual.optimized.glb",
"size": 9580212,
"checksum": "37800c26cdf784658a57dcc7328ab1b9b23e46d018be6d1e6420f127768b0332"
"size": 9580568,
"checksum": "ee7afd5e9179e6bcb7ec9171798466282e09d4ef696a3f9639bcd2bbc2ae8f48"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "grafu-castle-visual.glb",
"size": 11149716,
"sha256": "ba4552484ef9cddc1c00607d0dfd4c8fb0bae5b3cd3f992d215a580dac1e96ed",
"size": 11150124,
"sha256": "b775778f457d639e3f21982aaa6bd3ee1b0eba951c711d1ba319b0588e24a38f",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "grafu-castle-visual.optimized.glb",
"size": 11149716,
"checksum": "ba4552484ef9cddc1c00607d0dfd4c8fb0bae5b3cd3f992d215a580dac1e96ed"
"size": 11150124,
"checksum": "b775778f457d639e3f21982aaa6bd3ee1b0eba951c711d1ba319b0588e24a38f"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "hall-of-survivors-visual.glb",
"size": 15216768,
"sha256": "18b9d116da599159719ad76eeee45a4d89056ee8c61cc4f21cab9a1d9228fff7",
"size": 15216936,
"sha256": "96b91b24462b8428e8808295dcea29d26de9be8431ea6667dde92cdded4ddf2a",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "hall-of-survivors-visual.optimized.glb",
"size": 15216768,
"checksum": "18b9d116da599159719ad76eeee45a4d89056ee8c61cc4f21cab9a1d9228fff7"
"size": 15216936,
"checksum": "96b91b24462b8428e8808295dcea29d26de9be8431ea6667dde92cdded4ddf2a"
},
"compression": "meshopt+ktx2",
"commands": [
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.
@@ -3,7 +3,7 @@
"dungeonId": "hoto",
"title": "HOTO",
"status": "source-packaged",
"generatedAt": "2026-08-01T01:00:34.655Z",
"generatedAt": "2026-09-09T19:39:49.407Z",
"source": {
"recipe": "scripts/runewaker-pipeline/recipes/hoto.json",
"wdb": "Resource/wdb/dungeon/dgn_echoes_of_the_sea.wdb",
@@ -261,33 +261,37 @@
{
"role": "visual",
"fileName": "hoto-visual.glb",
"size": 6667208,
"sha256": "0d12b9c6dc477704d2833c81f76113d340da7f07f5885a40bafd1321524e455e",
"compression": "meshopt+ktx2"
"relativeFile": "hoto-visual.glb",
"size": 6710184,
"sha256": "beff3a180c55166d5ebd7e3610e65b59f8104edfd7596aa22ac3a49c578fcdc0"
},
{
"role": "collision",
"fileName": "hoto-collision-000.glb",
"relativeFile": "hoto-collision-000.glb",
"size": 3088036,
"sha256": "3116529f2d262ff4cab7997e852f9d15caacab6298921360b49dcafd52b8c475"
},
{
"role": "collision",
"fileName": "hoto-collision-001.glb",
"relativeFile": "hoto-collision-001.glb",
"size": 2989324,
"sha256": "1f5cbd4b04b5a060029fd7c023ee19db5d8646566fcd01b931473b5b13c13b9f"
},
{
"role": "collision",
"fileName": "hoto-collision-002.glb",
"relativeFile": "hoto-collision-002.glb",
"size": 526648,
"sha256": "0a468a6ef30941c04a20456866f0593f7e1f9f9bf8dc0024176d2128975f1879"
},
{
"role": "navigation",
"fileName": "hoto-navigation.glb",
"relativeFile": "hoto-navigation.glb",
"size": 482260,
"sha256": "2cd78e6d8f52cb6488747a1f87cb688eedb77ccf412fb50674c25f70306477fd"
"sha256": "a2bde2e4b8ed0d0bac8c4669a05192ff0a35ae9c64df0c0a29fe3735b65cbf4b"
}
],
"review": {
@@ -300,6 +304,6 @@
"exceptions": [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original skeletal animations and complex scripted mechanics remain later passes."
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work."
]
}
@@ -4,15 +4,11 @@
"status": "review-required",
"source": {
"file": "hoto-visual.glb",
"size": 26313948
"size": 26314004
},
"optimized": {
"file": "hoto-visual.optimized.glb",
"size": 6667208,
"checksum": "0d12b9c6dc477704d2833c81f76113d340da7f07f5885a40bafd1321524e455e"
"size": 6710184
},
"compression": "meshopt+ktx2",
"commands": [
"node scripts/asset-pipeline/compress-ktx2.mjs <optimized.glb> --concurrency 1 --jobs 2"
]
"compression": "meshopt+ktx2"
}
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"sourceWdb": "D:\\Projects\\rom-pvt-server-backups\\Runewaker\\Resource\\wdb\\dungeon\\dgn_echoes_of_the_sea.wdb",
"sourceWdb": "F:\\Projects\\rom-pvt-server-backups\\Runewaker\\Resource\\wdb\\dungeon\\dgn_echoes_of_the_sea.wdb",
"coordinateSystem": "runewaker-left-handed-y-up",
"descriptorCount": 1627,
"descriptors": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "ice-dwarf-kingdom-visual.glb",
"size": 9880192,
"sha256": "a6b902e85bb6026f18281d77efa235ec880ffc2fc13ea6bfdc431d24af768e00",
"size": 9880304,
"sha256": "41cef8ce1bf6dbf7df0c4e2797a325e4d50a3870d435d46684600894d4aa92e7",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "ice-dwarf-kingdom-visual.optimized.glb",
"size": 9880192,
"checksum": "a6b902e85bb6026f18281d77efa235ec880ffc2fc13ea6bfdc431d24af768e00"
"size": 9880304,
"checksum": "41cef8ce1bf6dbf7df0c4e2797a325e4d50a3870d435d46684600894d4aa92e7"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "kal-turok-hive-visual.glb",
"size": 1777616,
"sha256": "012867deefc4dc5a18c3e1a507309aa48f119cf4c2e88968fdd2787936ed7c24",
"size": 1777728,
"sha256": "75c0c596218cec07003e2def1d74c8e1f5ca3010ed370db558a1786b003f4f1c",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "kal-turok-hive-visual.optimized.glb",
"size": 1777616,
"checksum": "012867deefc4dc5a18c3e1a507309aa48f119cf4c2e88968fdd2787936ed7c24"
"size": 1777728,
"checksum": "75c0c596218cec07003e2def1d74c8e1f5ca3010ed370db558a1786b003f4f1c"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -3,7 +3,7 @@
"dungeonId": "kalins-shrine",
"title": "Kalin's Shrine",
"status": "source-packaged",
"generatedAt": "2026-08-01T00:57:43.432Z",
"generatedAt": "2026-09-09T18:51:01.674Z",
"source": {
"recipe": "scripts/runewaker-pipeline/recipes/kalins-shrine.json",
"wdb": "Resource/wdb/dungeon/dgn_kleeon_temple.wdb",
@@ -277,33 +277,37 @@
{
"role": "visual",
"fileName": "kalins-shrine-visual.glb",
"size": 8747072,
"sha256": "769accb48e23c9f80e28697e3e8454ef4148243a69ce247ed06ba4eb35bb0da7",
"compression": "meshopt+ktx2"
"relativeFile": "kalins-shrine-visual.glb",
"size": 8851724,
"sha256": "023c43a56253e97fa0526185c61f76e0e9fa8398b7a35771728566c331cd22a9"
},
{
"role": "collision",
"fileName": "kalins-shrine-collision-000.glb",
"relativeFile": "kalins-shrine-collision-000.glb",
"size": 4748976,
"sha256": "ed1e713c159e024b4786ef8befadbfedd3849ba580e7f49970e2785b6f4fff7d"
},
{
"role": "collision",
"fileName": "kalins-shrine-collision-001.glb",
"relativeFile": "kalins-shrine-collision-001.glb",
"size": 4555800,
"sha256": "5fea14212353593c8daced2e303f88dae010f8f73aedb9890a3eee67eb8dd7cd"
},
{
"role": "collision",
"fileName": "kalins-shrine-collision-002.glb",
"relativeFile": "kalins-shrine-collision-002.glb",
"size": 1215728,
"sha256": "e51d48d1f4521f1a4722fd4c528a90bdfd2978a87d2ace0dd662f23475490565"
},
{
"role": "navigation",
"fileName": "kalins-shrine-navigation.glb",
"relativeFile": "kalins-shrine-navigation.glb",
"size": 327792,
"sha256": "37fd7f8efac36f83279efbfb33df37646453af214db29597bd14c8e9f19ddc04"
"sha256": "6b77496f7b46890d401f955f9254ae37d25d9a1287a3bc9e787e9a254f9d9071"
}
],
"review": {
@@ -316,6 +320,6 @@
"exceptions": [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original skeletal animations and complex scripted mechanics remain later passes."
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work."
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 892 KiB

After

Width:  |  Height:  |  Size: 892 KiB

@@ -4,15 +4,11 @@
"status": "review-required",
"source": {
"file": "kalins-shrine-visual.glb",
"size": 26551164
"size": 26551296
},
"optimized": {
"file": "kalins-shrine-visual.optimized.glb",
"size": 8747072,
"checksum": "769accb48e23c9f80e28697e3e8454ef4148243a69ce247ed06ba4eb35bb0da7"
"size": 8851724
},
"compression": "meshopt+ktx2",
"commands": [
"node scripts/asset-pipeline/compress-ktx2.mjs <optimized.glb> --concurrency 1 --jobs 2"
]
"compression": "meshopt+ktx2"
}
@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"sourceWdb": "D:\\Projects\\rom-pvt-server-backups\\Runewaker\\Resource\\wdb\\dungeon\\dgn_kleeon_temple.wdb",
"sourceWdb": "F:\\Projects\\rom-pvt-server-backups\\Runewaker\\Resource\\wdb\\dungeon\\dgn_kleeon_temple.wdb",
"coordinateSystem": "runewaker-left-handed-y-up",
"descriptorCount": 943,
"descriptors": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "kawaks-tomb-visual.glb",
"size": 8963188,
"sha256": "de54d223725e2c3dfb0c47a97c208a212ef9423860bd0ac390f1482b302d3a32",
"size": 8963376,
"sha256": "3f1755780540c54a1ba9b528e646ebd39b12ab127e41fa2d497fd7749d202018",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "kawaks-tomb-visual.optimized.glb",
"size": 8963188,
"checksum": "de54d223725e2c3dfb0c47a97c208a212ef9423860bd0ac390f1482b302d3a32"
"size": 8963376,
"checksum": "3f1755780540c54a1ba9b528e646ebd39b12ab127e41fa2d497fd7749d202018"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "kulech-bones-nest-visual.glb",
"size": 12146892,
"sha256": "2d912dcf7c0c8e59e36362fd947df1d5554c5dd5060590e1abb112e4c39f606f",
"size": 12147008,
"sha256": "b06b42b3c7f19abf9d645eec6be78f868cf6966e0b472e2ca4a1c36d3cf4d2d1",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "kulech-bones-nest-visual.optimized.glb",
"size": 12146892,
"checksum": "2d912dcf7c0c8e59e36362fd947df1d5554c5dd5060590e1abb112e4c39f606f"
"size": 12147008,
"checksum": "b06b42b3c7f19abf9d645eec6be78f868cf6966e0b472e2ca4a1c36d3cf4d2d1"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "lair-demon-dragon-visual.glb",
"size": 4551300,
"sha256": "d28481504802203e800985836f2b0907d67f6d6d0495c67500914e749d771ffe",
"size": 4551356,
"sha256": "b371465e96ef978915c9a515846ecf9dccb98607fd549617a1c71f45468fa5a7",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "lair-demon-dragon-visual.optimized.glb",
"size": 4551300,
"checksum": "d28481504802203e800985836f2b0907d67f6d6d0495c67500914e749d771ffe"
"size": 4551356,
"checksum": "b371465e96ef978915c9a515846ecf9dccb98607fd549617a1c71f45468fa5a7"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "mystic-altar-visual.glb",
"size": 5552872,
"sha256": "765dd1b15bbe79fe1ffc2104e10caece19acaf86c919f0327419cb3609dbf600",
"size": 5552892,
"sha256": "c37f27643598c8fe6401828be2950bc08f299dfe491df37736fda4662c70a96e",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "mystic-altar-visual.optimized.glb",
"size": 5552872,
"checksum": "765dd1b15bbe79fe1ffc2104e10caece19acaf86c919f0327419cb3609dbf600"
"size": 5552892,
"checksum": "c37f27643598c8fe6401828be2950bc08f299dfe491df37736fda4662c70a96e"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -3,7 +3,7 @@
"dungeonId": "paspers-shrine",
"title": "Pasper's Shrine",
"status": "source-packaged",
"generatedAt": "2026-08-01T01:00:57.399Z",
"generatedAt": "2026-09-09T19:19:45.719Z",
"source": {
"recipe": "scripts/runewaker-pipeline/recipes/paspers-shrine.json",
"wdb": "Resource/wdb/dungeon/dgn_borsburs.wdb",
@@ -137,27 +137,30 @@
{
"role": "visual",
"fileName": "paspers-shrine-visual.glb",
"size": 5625952,
"sha256": "82ea6cafd19c0a323c994795699223a07193807da29b300eb12dac41146f1034",
"compression": "meshopt+ktx2"
"relativeFile": "paspers-shrine-visual.glb",
"size": 5674908,
"sha256": "bcf6443e8adb8b7f7dd81f4a97b79aac1c99756a83289bbada31853d2d9adb7c"
},
{
"role": "collision",
"fileName": "paspers-shrine-collision-000.glb",
"relativeFile": "paspers-shrine-collision-000.glb",
"size": 4291972,
"sha256": "b6efbe1b90545dd2c2af3bfc7396616147211f6f79b999ccf81ca3680fdc984e"
},
{
"role": "collision",
"fileName": "paspers-shrine-collision-001.glb",
"relativeFile": "paspers-shrine-collision-001.glb",
"size": 4479896,
"sha256": "1c6f52d7e21171acfbe3fb43335b2c7f3de13217881840947f15fd0722adb4de"
},
{
"role": "navigation",
"fileName": "paspers-shrine-navigation.glb",
"relativeFile": "paspers-shrine-navigation.glb",
"size": 152356,
"sha256": "e05fb0b7a3c86bba3ac46606365169dea6c575281b2bb15707addcedb279f108"
"sha256": "4ebba3fb97a99df49c7301dbfcdb8154634cf4e56a85c5ed587c7515221d0a10"
}
],
"review": {
@@ -170,6 +173,6 @@
"exceptions": [
"Secondary WDB doodads/effects remain in the audit; this pass exports the primary ROS hierarchy.",
"Collision initially includes decorative primary-ROS geometry.",
"Original skeletal animations and complex scripted mechanics remain later passes."
"Original GR2 actor animations are recovered where source graphs and validation permit; scripted encounter mechanics remain evidence-driven per-instance work."
]
}
@@ -4,15 +4,11 @@
"status": "review-required",
"source": {
"file": "paspers-shrine-visual.glb",
"size": 14723576
"size": 14723652
},
"optimized": {
"file": "paspers-shrine-visual.optimized.glb",
"size": 5625952,
"checksum": "82ea6cafd19c0a323c994795699223a07193807da29b300eb12dac41146f1034"
"size": 5674908
},
"compression": "meshopt+ktx2",
"commands": [
"node scripts/asset-pipeline/compress-ktx2.mjs <optimized.glb> --concurrency 1 --jobs 2"
]
"compression": "meshopt+ktx2"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1024 KiB

After

Width:  |  Height:  |  Size: 1024 KiB

@@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"sourceWdb": "D:\\Projects\\rom-pvt-server-backups\\Runewaker\\Resource\\wdb\\dungeon\\dgn_borsburs.wdb",
"sourceWdb": "F:\\Projects\\rom-pvt-server-backups\\Runewaker\\Resource\\wdb\\dungeon\\dgn_borsburs.wdb",
"coordinateSystem": "runewaker-left-handed-y-up",
"descriptorCount": 564,
"descriptors": [
@@ -149,8 +149,8 @@
"role": "visual",
"fileName": "raksha-temple-visual.glb",
"relativeFile": "raksha-temple-visual.glb",
"size": 23554528,
"sha256": "3a61098e772a0c656aa25d88aee357f78b84c738e45150fca6fd5059c2b675f7"
"size": 23556584,
"sha256": "1fad5149acfce870be822f5355369f15dd76fba280bbeda31ff7f371c442082f"
},
{
"role": "collision",
@@ -8,7 +8,7 @@
},
"optimized": {
"file": "raksha-temple-visual.optimized.glb",
"size": 23554528
"size": 23556584
},
"compression": "meshopt+ktx2"
}
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "sardo-castle-visual.glb",
"size": 7756568,
"sha256": "16d759af9f8a3f98281e89362c7fb52dceff6543bbd837a7c6f89015920cdd94",
"size": 7756828,
"sha256": "191396625e301a41afab38e168848a198a9be5918a071dadda841683a7c6261e",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "sardo-castle-visual.optimized.glb",
"size": 7756568,
"checksum": "16d759af9f8a3f98281e89362c7fb52dceff6543bbd837a7c6f89015920cdd94"
"size": 7756828,
"checksum": "191396625e301a41afab38e168848a198a9be5918a071dadda841683a7c6261e"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -149,8 +149,8 @@
"role": "visual",
"fileName": "the-origin-visual.glb",
"relativeFile": "the-origin-visual.glb",
"size": 28683180,
"sha256": "d1ded0316b2cbaa07458c56864c47c01afaa85e4f33dd13f5d95984d7305fb92"
"size": 28684736,
"sha256": "1f96c2b57a24470a003834a8c91cb0ceda5b01824d5bfa31904afd247c404e88"
},
{
"role": "collision",
@@ -8,7 +8,7 @@
},
"optimized": {
"file": "the-origin-visual.optimized.glb",
"size": 28683180
"size": 28684736
},
"compression": "meshopt+ktx2"
}
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "tomb-seven-heroes-visual.glb",
"size": 10910804,
"sha256": "d9f39da6b61c9f841c5d075e4985bc67e533ce422313fdf38d5f6c99dbb304c1",
"size": 10911104,
"sha256": "09250a1fbabf8a03f922c9a3950aee3daa1edc54215cd03ddc9bf5c96e1429bd",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "tomb-seven-heroes-visual.optimized.glb",
"size": 10910804,
"checksum": "d9f39da6b61c9f841c5d075e4985bc67e533ce422313fdf38d5f6c99dbb304c1"
"size": 10911104,
"checksum": "09250a1fbabf8a03f922c9a3950aee3daa1edc54215cd03ddc9bf5c96e1429bd"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "treasure-trove-visual.glb",
"size": 7433280,
"sha256": "f4c4dc12df65f8afc3c696308db17cb42b6a5894086dbe870dae2e21e1258448",
"size": 7433780,
"sha256": "c56d429187fd2ffbe20e11d5e7bbfd2128a779c9251fa2ab4b12286caf0de0b6",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "treasure-trove-visual.optimized.glb",
"size": 7433280,
"checksum": "f4c4dc12df65f8afc3c696308db17cb42b6a5894086dbe870dae2e21e1258448"
"size": 7433780,
"checksum": "c56d429187fd2ffbe20e11d5e7bbfd2128a779c9251fa2ab4b12286caf0de0b6"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "warnorken-arena-visual.glb",
"size": 3752136,
"sha256": "c452a843c2e4123d4fe0d20da41558f5b5a1906548b3d091dc691ffc42ed19d9",
"size": 3752248,
"sha256": "ee81c9e7c94d624977143297ca6c070c40799fdda1c51b92044c70dff53987ea",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "warnorken-arena-visual.optimized.glb",
"size": 3752136,
"checksum": "c452a843c2e4123d4fe0d20da41558f5b5a1906548b3d091dc691ffc42ed19d9"
"size": 3752248,
"checksum": "ee81c9e7c94d624977143297ca6c070c40799fdda1c51b92044c70dff53987ea"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "ystra-labyrinth-boss1-visual.glb",
"size": 2709352,
"sha256": "c8ca72a5fdf487fa71a1aecd1e66ea821183f476540be9a89444b409be957c25",
"size": 2709464,
"sha256": "5e35eeccf5e207477bfb3a38dee75d53a5c5467433a90270b79a9ba8619a6260",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "ystra-labyrinth-boss1-visual.optimized.glb",
"size": 2709352,
"checksum": "c8ca72a5fdf487fa71a1aecd1e66ea821183f476540be9a89444b409be957c25"
"size": 2709464,
"checksum": "5e35eeccf5e207477bfb3a38dee75d53a5c5467433a90270b79a9ba8619a6260"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "ystra-labyrinth-boss2-visual.glb",
"size": 2682688,
"sha256": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab",
"size": 2682800,
"sha256": "be717e3d6e6301ff60762552283386d73da04240e6a7d1e36933e83e02d0250a",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "ystra-labyrinth-boss2-visual.optimized.glb",
"size": 2682688,
"checksum": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab"
"size": 2682800,
"checksum": "be717e3d6e6301ff60762552283386d73da04240e6a7d1e36933e83e02d0250a"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "ystra-labyrinth-boss3-visual.glb",
"size": 2682688,
"sha256": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab",
"size": 2682800,
"sha256": "be717e3d6e6301ff60762552283386d73da04240e6a7d1e36933e83e02d0250a",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "ystra-labyrinth-boss3-visual.optimized.glb",
"size": 2682688,
"checksum": "d52524621220e16d844badc24ee745f4374798031b2b0bc7236decbe3b9588ab"
"size": 2682800,
"checksum": "be717e3d6e6301ff60762552283386d73da04240e6a7d1e36933e83e02d0250a"
},
"compression": "meshopt+ktx2",
"commands": [
@@ -137,8 +137,8 @@
{
"role": "visual",
"fileName": "zurhidon-stronghold-124-visual.glb",
"size": 17189492,
"sha256": "a2d450946ca9687395cf65815b931c7817d9f41041afeb36ba1a351ec24104c4",
"size": 17189904,
"sha256": "58be5be365b14e42772a4560c5c1d10fceb8aa8abb2c1771cc21ba2b2e02872a",
"compression": "meshopt+ktx2"
},
{
@@ -8,8 +8,8 @@
},
"optimized": {
"file": "zurhidon-stronghold-124-visual.optimized.glb",
"size": 17189492,
"checksum": "a2d450946ca9687395cf65815b931c7817d9f41041afeb36ba1a351ec24104c4"
"size": 17189904,
"checksum": "58be5be365b14e42772a4560c5c1d10fceb8aa8abb2c1771cc21ba2b2e02872a"
},
"compression": "meshopt+ktx2",
"commands": [
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more