map render dedupe fixes

This commit is contained in:
Skillbert
2024-05-02 19:59:31 +02:00
parent f18196a045
commit c2f80c4024
8 changed files with 93 additions and 60 deletions

View File

@@ -18,6 +18,7 @@ import { MaterialData } from "./jmat";
import { legacyMajors } from "../cache/legacycache";
import { classicGroups } from "../cache/classicloader";
import { mapImageCamera } from "../map";
import { findImageBounds, pixelsToImageFile, sliceImage } from "../imgutils";
export type SimpleModelDef = {
@@ -404,6 +405,7 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData, changed:
toggles: Record<string, boolean> = {};
chunkx: number;
chunkz: number;
globalname = "";
constructor(cache: ThreejsSceneCache, preparsed: ReturnType<typeof parseMapsquare>, chunkx: number, chunkz: number, opts: ParsemapOpts) {
super();
@@ -441,8 +443,9 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData, changed:
group.traverse(q => q.layers.set(1));
this.loaded.chunkroot.add(group);
let cam = mapImageCamera(loc.x + this.rootnode.position.x / tiledimensions - 16, loc.z + this.rootnode.position.z / tiledimensions - 16, 32, 0.15, 0.25);
let img = await this.renderscene!.takeMapPicture(cam, -1, -1, false, group);
// let cam = mapImageCamera(loc.x + this.rootnode.position.x / tiledimensions - 16, loc.z + this.rootnode.position.z / tiledimensions - 16, 32, 0.15, 0.25);
let cam = this.renderscene!.getCurrent2dCamera()!;
let img = await this.renderscene!.takeMapPicture(cam, 256, 256, false, group);
group.removeFromParent();
model.map(q => q.mesh.setSectionHide(q, false));
return img;
@@ -466,8 +469,10 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData, changed:
cleanup() {
this.listeners = {};
delete globalThis[`chunk_${this.chunkx}_${this.chunkz}`];
if (this.globalname) {
delete globalThis[this.globalname];
this.globalname = "";
}
//only clear vertex memory for now, materials might be reused and are up to the scenecache
this.chunkdata.then(q => q.chunkroot.traverse(obj => {
if (obj instanceof Mesh) { obj.geometry.dispose(); }
@@ -494,7 +499,14 @@ export class RSMapChunk extends TypedEmitter<{ loaded: RSMapChunkData, changed:
this.renderscene = scene;
scene.addSceneElement(this);
globalThis[`chunk_${this.chunkx}_${this.chunkz}`] = this;
for (let i = 0; i < 10; i++) {
let name = `chunk_${this.chunkx}_${this.chunkz}${i == 0 ? "" : `_${i}`}`;
if (!globalThis[name]) {
globalThis[name] = this;
this.globalname = name;
break;
}
}
}
onModelLoaded() {

View File

@@ -76,9 +76,8 @@ export function validOpenrs2Caches() {
1480,
644,257,//incomplete textures
1456,//missing materials
1456,1665,//missing materials
1479,//missing items could probably be worked around
//254,255,667,256,257,1479,3
];
let allcaches: Openrs2CacheMeta[] = await fetch(`${endpoint}/caches.json`).then(q => q.json());
let checkedcaches = allcaches.filter(q =>

View File

@@ -151,7 +151,7 @@ export async function renderAppearance(scene: ThreejsSceneCache, mode: "player"
render.setCameraLimits(new Vector3(0, 0.85, 0));
let modelfile = Buffer.from(await exportThreeJsGltf(render.getModelNode()));
let img = await render.takeCanvasPicture();
let img = await render.takeScenePicture();
let imgfile = await pixelsToImageFile(img, "png", 1);
render.dispose();

View File

@@ -10,6 +10,7 @@ import { RenderedMapMeta } from ".";
import { crc32addInt } from "../libs/crc32util";
import type { RSMapChunk } from "../3d/modelnodes";
import { getOrInsert } from "../utils";
import { ThreeJsRenderer } from "../viewer/threejsrender";
export function getLocImageHash(grid: TileGrid, info: WorldLocation) {
let loc = info.location;
@@ -465,6 +466,29 @@ export async function generateFloorHashBoxes(scene: ThreejsSceneCache, grid: Til
return group;
}
export function pointsIntersectProjection(projection: Matrix4, points: number[][]) {
//make them local vars to prevent writing into old space
const min = new Vector3();
const max = new Vector3();
const tmp = new Vector3();
for (let group of points) {
for (let i = 0; i < group.length; i += 3) {
tmp.set(group[i + 0], group[i + 1], group[i + 2]);
tmp.applyMatrix4(projection);
if (i == 0) {
min.copy(tmp);
max.copy(tmp);
} else {
min.min(tmp);
max.max(tmp);
}
}
if (min.x < 1 && max.x > -1 && min.y < 1 && max.y > -1) {
return true;
}
}
return false;
}
/**
* this class is wayyy overkill for what is currently used
@@ -473,30 +497,6 @@ export class ImageDiffGrid {
gridsize = 64;
grid = new Uint8Array(this.gridsize * this.gridsize);
anyInside(projection: Matrix4, points: number[][]) {
//make them local vars to prevent writing into old space
const min = new Vector3();
const max = new Vector3();
const tmp = new Vector3();
for (let group of points) {
for (let i = 0; i < group.length; i += 3) {
tmp.set(group[i + 0], group[i + 1], group[i + 2]);
tmp.applyMatrix4(projection);
if (i == 0) {
min.copy(tmp);
max.copy(tmp);
} else {
min.min(tmp);
max.max(tmp);
}
}
if (min.x < 1 && max.x > -1 && min.y < 1 && max.y > -1) {
return true;
}
}
return false;
}
addPolygons(projection: Matrix4, points: number[][]) {
const v0 = new Vector3();
const v1 = new Vector3();
@@ -717,7 +717,28 @@ globalThis.test = async (chunka: RSMapChunk, levela: number, levelb = 0, chunkb
chunka.emit("changed", undefined);
return chunka;
return () => {
let render = globalThis.render as ThreeJsRenderer;
let cam = render.getCurrent2dCamera();
if (!cam) { return; }
chunka.rootnode.updateWorldMatrix(true, false);
let modelmatrix = new Matrix4().makeTranslation(
chunka.chunkx * tiledimensions * chunka.loaded!.chunkSize,
0,
chunka.chunkz * tiledimensions * chunka.loaded!.chunkSize,
).premultiply(chunka.rootnode.matrixWorld);
let proj = cam.projectionMatrix.clone()
.multiply(cam.matrixWorldInverse)
.multiply(modelmatrix);
let locschanged = pointsIntersectProjection(proj, cmplocs);
let floorchanged = pointsIntersectProjection(proj, cmpfloor);
let anychanged = locschanged || floorchanged;
return { locschanged, floorchanged, anychanged };
}
}
function modelPlacementHash(loc: WorldLocation) {

View File

@@ -12,7 +12,7 @@ import { ScriptOutput } from "../scriptrunner";
import { AsyncReturnType, CallbackPromise, delay, stringToFileRange, trickleTasks } from "../utils";
import { drawCollision } from "./collisionimage";
import prettyJson from "json-stringify-pretty-compact";
import { ChunkRenderMeta, chunkSummary, compareFloorDependencies, compareLocDependencies, ImageDiffGrid, mapsquareFloorDependencies, mapsquareLocDependencies, RenderDepsTracker, RenderDepsVersionInstance } from "./chunksummary";
import { ChunkRenderMeta, chunkSummary, compareFloorDependencies, compareLocDependencies, mapsquareFloorDependencies, mapsquareLocDependencies, pointsIntersectProjection, RenderDepsTracker, RenderDepsVersionInstance } from "./chunksummary";
import { RSMapChunk } from "../3d/modelnodes";
import * as zlib from "zlib";
import { Camera, Matrix4, Object3D, OrthographicCamera, Vector3 } from "three";
@@ -708,7 +708,7 @@ const rendermodeInteractions: RenderMode<"interactions"> = function (engine, con
let rect = { x: singlerect.x * loaded.chunkSize, z: singlerect.z * loaded.chunkSize, xsize: loaded.chunkSize, zsize: loaded.chunkSize };
let { hashes, locdatas, locs } = chunkSummary(loaded.grid, loaded.modeldata, rect);
let emptyimagecount = 0;
let hashimgs: Record<number, { img: string, center: number[], loc: number, dx: number, dy: number }> = {};
let hashimgs: Record<number, { img: string, center: number[], loc: number, dx: number, dy: number, w: number, h: number }> = {};
for (let [hash, { center, locdata }] of hashes) {
let ops = [locdata.location.actions_0, locdata.location.actions_1, locdata.location.actions_2, locdata.location.actions_3, locdata.location.actions_4].filter((q): q is string => !!q);
let model = loaded.locRenders.get(locdata);
@@ -728,7 +728,6 @@ const rendermodeInteractions: RenderMode<"interactions"> = function (engine, con
let ypos = baseheight / tiledimensions + center[1];
let cam = mapImageCamera(locdata.x + center[0] + ypos * thiscnf.dxdy - ntiles / 2, locdata.z + center[2] + ypos * thiscnf.dzdy - ntiles / 2, ntiles, thiscnf.dxdy, thiscnf.dzdy);
let img = await renderer.renderer.takeMapPicture(cam, ntiles * thiscnf.pxpersquare, ntiles * thiscnf.pxpersquare, false, group);
flipImage(img);
group.removeFromParent();
model.map(q => q.mesh.setSectionHide(q, false));
@@ -746,6 +745,8 @@ const rendermodeInteractions: RenderMode<"interactions"> = function (engine, con
loc: locdata.locid,
dx: bounds.x - img.width / 2,
dy: bounds.y - img.height / 2,
w: bounds.width,
h: bounds.height,
center,
img: `data:image/${format};base64,${imgfile.toString("base64")}`
};
@@ -804,7 +805,6 @@ const rendermode3d: RenderMode<"3d" | "minimap"> = function (engine, config, cnf
findparent: for (let parentoption of parentCandidates) {
optloop: for (let versionMatch of await parentinfo.findMatches(this.datarect, parentoption.name)) {
let diff = new ImageDiffGrid();
let isdirty = false;
for (let chunk of chunks) {
let other = versionMatch.metas.find(q => q.x == chunk.x && q.z == chunk.z);
@@ -827,8 +827,8 @@ const rendermode3d: RenderMode<"3d" | "minimap"> = function (engine, config, cnf
// if (locs.length + floor.length > 400) {
// continue optloop;
// }
isdirty ||= diff.anyInside(proj, locs);
isdirty ||= diff.anyInside(proj, floor);
isdirty ||= pointsIntersectProjection(proj, locs);
isdirty ||= pointsIntersectProjection(proj, floor);
if (isdirty) {
break;
}
@@ -852,7 +852,6 @@ const rendermode3d: RenderMode<"3d" | "minimap"> = function (engine, config, cnf
let img: ImageData | null = null;
if (!parentFile) {
img = await renderer.renderer.takeMapPicture(cam, tiles * pxpersquare, tiles * pxpersquare, thiscnf.mode == "minimap");
flipImage(img);
// isImageEmpty(img, "black");
//keep reference to dedupe similar renders

View File

@@ -1062,7 +1062,7 @@ function ExportSceneMenu(p: { ctx: UIContextReady, renderopts: ThreeJsSceneEleme
let changeImg = async (instCrop = cropimg, instSize = imgsize) => {
if (p.renderopts!.camMode == "vr360") { instCrop = false; }
let newpixels = await p.ctx.renderer.takeCanvasPicture(instSize.w || undefined, instSize.h || undefined);
let newpixels = await p.ctx.renderer.takeScenePicture(instSize.w || undefined, instSize.h || undefined);
let newimg = makeImageData(newpixels.data, newpixels.width, newpixels.height);
let cnv = document.createElement("canvas");
let ctx = cnv.getContext("2d")!;

View File

@@ -326,7 +326,7 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
}
@boundMethod
async guaranteeGlCalls(glfunction: () => void | Promise<void>) {
async guaranteeGlCalls<T>(glfunction: () => T | Promise<T>): Promise<T> {
let waitContext = () => {
if (!this.renderer.getContext().isContextLost()) {
return;
@@ -345,7 +345,7 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
await waitContext();
//it seems like the first render after a context loss is always failed, force 2 renders this way
let prerenderlosses = this.contextLossCountLastRender;
await glfunction();
let res = await glfunction();
//new stack frame to let all errors resolve
await delay(1);
@@ -356,7 +356,7 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
console.log("lost and regained context during render " + new Date());
continue;
}
return;
return res;
}
throw new Error("Failed to render frame after 5 retries");
}
@@ -435,7 +435,7 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
}
}
async takeCanvasPicture(width = this.canvas.width, height = this.canvas.height) {
async takeScenePicture(width = this.canvas.width, height = this.canvas.height) {
let rendertarget: THREE.WebGLRenderTarget | null = null;
if (width != this.canvas.width || height != this.canvas.height) {
let gl = this.renderer.getContext();
@@ -448,7 +448,7 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
});
// (rendertarget as any).isXRRenderTarget = true;
}
await this.guaranteeGlCalls(() => {
return this.guaranteeGlCalls(() => {
let oldtarget = this.renderer.getRenderTarget();
this.renderer.setRenderTarget(rendertarget);
this.resizeViewToRendererSize();
@@ -465,7 +465,14 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
}
this.renderer.setRenderTarget(oldtarget);
this.resizeViewToRendererSize();
return this.getFrameBufferPixels();
});
}
getFrameBufferPixels() {
let rendertarget = this.renderer.getRenderTarget();
let width = rendertarget?.width ?? this.canvas.width;
let height = rendertarget?.height ?? this.canvas.height;
let buf = new Uint8Array(width * height * 4);//node-gl doesn't accept clamped
if (rendertarget) {
this.renderer.readRenderTargetPixels(rendertarget as any, 0, 0, width, height, buf);
@@ -479,12 +486,11 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
return r;
}
async takeMapPicture(cam: Camera, framesizex = -1, framesizey = -1, linearcolor = false, highlight: Object3D | null = null) {
takeMapPicture(cam: Camera, framesizex = -1, framesizey = -1, linearcolor = false, highlight: Object3D | null = null) {
if (framesizex != -1 && framesizey != -1) {
this.renderer.setSize(framesizex, framesizey);
}
let img: ImageData | null = null;
await this.guaranteeGlCalls(() => {
return this.guaranteeGlCalls(() => {
let opaqueBackground = !highlight;
//change render settings
let oldcolorspace = this.renderer.outputColorSpace;
@@ -492,7 +498,6 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
this.renderer.setClearColor(new THREE.Color(0, 0, 0), (opaqueBackground ? 255 : 0));
this.scene.background = (opaqueBackground ? new THREE.Color(0, 0, 0) : null);
let ctx = this.renderer.getContext();
if (!highlight) {
this.renderScene(cam);
} else {
@@ -504,16 +509,15 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
cam.layers.mask = old;
}
let pixelbuffer = new Uint8ClampedArray(ctx.canvas.width * ctx.canvas.height * 4);
ctx.readPixels(0, 0, ctx.canvas.width, ctx.canvas.height, ctx.RGBA, ctx.UNSIGNED_BYTE, pixelbuffer);
img = makeImageData(pixelbuffer, ctx.canvas.width, ctx.canvas.height);
let img = this.getFrameBufferPixels();
//restore render settings
this.renderer.outputColorSpace = oldcolorspace;
this.renderer.setClearColor(new THREE.Color(0, 0, 0), 0);
this.scene.background = null;
return img;
});
return img!;
}
setCameraPosition(pos: Vector3) {
@@ -697,13 +701,10 @@ export class ThreeJsRenderer extends TypedEmitter<ThreeJsRendererEvents>{
this.renderer.clearColor();
this.renderer.clearDepth();
this.renderer.render(scene, itemcam);
this.renderer.setRenderTarget(oldtarget);
let img = this.getFrameBufferPixels()
let buf = new Uint8Array(width * height * 4);//node-gl doesn't accept clamped
this.renderer.readRenderTargetPixels(rendertarget, 0, 0, width, height, buf);
let r = makeImageData(buf, width, height);
flipImage(r);
return r;
this.renderer.setRenderTarget(oldtarget);
return img;
}
let dispose = () => rendertarget?.dispose();

1
todo.md Normal file
View File

@@ -0,0 +1 @@
make a database of model file id+hash and which matetial ids they use in order to diff material changes in incremental map renders