From c2f80c4024acaee1b48ad8972d6ee765bf4cd945 Mon Sep 17 00:00:00 2001 From: Skillbert Date: Thu, 2 May 2024 19:59:31 +0200 Subject: [PATCH] map render dedupe fixes --- src/3d/modelnodes.ts | 22 +++++++++--- src/cache/openrs2loader.ts | 3 +- src/headless/api.ts | 2 +- src/map/chunksummary.ts | 71 ++++++++++++++++++++++++------------- src/map/index.ts | 13 ++++--- src/viewer/scenenodes.tsx | 2 +- src/viewer/threejsrender.ts | 39 ++++++++++---------- todo.md | 1 + 8 files changed, 93 insertions(+), 60 deletions(-) create mode 100644 todo.md diff --git a/src/3d/modelnodes.ts b/src/3d/modelnodes.ts index 08be1bd..a7b593f 100644 --- a/src/3d/modelnodes.ts +++ b/src/3d/modelnodes.ts @@ -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 = {}; chunkx: number; chunkz: number; + globalname = ""; constructor(cache: ThreejsSceneCache, preparsed: ReturnType, 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() { diff --git a/src/cache/openrs2loader.ts b/src/cache/openrs2loader.ts index df87820..69e565e 100644 --- a/src/cache/openrs2loader.ts +++ b/src/cache/openrs2loader.ts @@ -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 => diff --git a/src/headless/api.ts b/src/headless/api.ts index 0f94263..c053363 100644 --- a/src/headless/api.ts +++ b/src/headless/api.ts @@ -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(); diff --git a/src/map/chunksummary.ts b/src/map/chunksummary.ts index 1450c30..5027a33 100644 --- a/src/map/chunksummary.ts +++ b/src/map/chunksummary.ts @@ -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) { diff --git a/src/map/index.ts b/src/map/index.ts index b589611..f4e4487 100644 --- a/src/map/index.ts +++ b/src/map/index.ts @@ -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 = {}; + let hashimgs: Record = {}; 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 diff --git a/src/viewer/scenenodes.tsx b/src/viewer/scenenodes.tsx index a0148af..3d08b3c 100644 --- a/src/viewer/scenenodes.tsx +++ b/src/viewer/scenenodes.tsx @@ -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")!; diff --git a/src/viewer/threejsrender.ts b/src/viewer/threejsrender.ts index 475bef0..a11b5da 100644 --- a/src/viewer/threejsrender.ts +++ b/src/viewer/threejsrender.ts @@ -326,7 +326,7 @@ export class ThreeJsRenderer extends TypedEmitter{ } @boundMethod - async guaranteeGlCalls(glfunction: () => void | Promise) { + async guaranteeGlCalls(glfunction: () => T | Promise): Promise { let waitContext = () => { if (!this.renderer.getContext().isContextLost()) { return; @@ -345,7 +345,7 @@ export class ThreeJsRenderer extends TypedEmitter{ 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{ 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{ } } - 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{ }); // (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{ } 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{ 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{ 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{ 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{ 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(); diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..0745a76 --- /dev/null +++ b/todo.md @@ -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 \ No newline at end of file