diff --git a/src/SplatPager.ts b/src/SplatPager.ts index 3d53d743..ea753e7c 100644 --- a/src/SplatPager.ts +++ b/src/SplatPager.ts @@ -15,7 +15,7 @@ import { type SplatEncoding, SplatFileType, } from "./defines"; -import { pagedSplatTexCoord } from "./dyno"; +import { type DynoUsampler2DArray, pagedSplatTexCoord } from "./dyno"; import { decodeExtSplat, getTextureSize, unpackSplat } from "./utils"; import * as wasm from "./wasm"; @@ -29,6 +29,10 @@ export interface PagedSplatsOptions { maxSh?: number; } +const PAGE_WIDTH = 256; +const PAGE_HEIGHT = 256; +const PAGE_SPLATS = PAGE_WIDTH * PAGE_HEIGHT; // 65536 + export class PagedSplats implements SplatSource { pager?: SplatPager; rootUrl: string; @@ -507,15 +511,23 @@ export interface SplatPagerOptions { numFetchers?: number; } +interface PageUpload { + page: number; + numSplats: number; + packedArray: Uint32Array; + extArray?: Uint32Array; + shArrays: Array; +} + export class SplatPager { - renderer: THREE.WebGLRenderer; + readonly renderer: THREE.WebGLRenderer; - extSplats: boolean; - maxPages: number; - maxSplats: number; - pageSplats: number; + readonly extSplats: boolean; + readonly maxPages: number; + readonly maxSplats: number; + readonly pageSplats: number; - maxSh: number; + readonly maxSh: number; curSh: number; autoDrive: boolean; @@ -533,20 +545,8 @@ export class SplatPager { pageFreelist: number[]; pageLru: Set<{ page: number; lru: number }>; freeablePages: number[]; - newUploads: { - page: number; - numSplats: number; - packedArray: Uint32Array; - extArray?: Uint32Array; - extra: Record; - }[]; - readyUploads: { - page: number; - numSplats: number; - packedArray: Uint32Array; - extArray?: Uint32Array; - extra: Record; - }[]; + newUploads: PageUpload[]; + readyUploads: PageUpload[]; lodTreeUpdates: { splats: PagedSplats; page: number; @@ -568,11 +568,12 @@ export class SplatPager { THREE.DataArrayTexture >; extTexture: dyno.DynoUsampler2DArray<"extTexture", THREE.DataArrayTexture>; - - sh1Texture: dyno.DynoUsampler2DArray<"sh1", THREE.DataArrayTexture>; - sh2Texture: dyno.DynoUsampler2DArray<"sh2", THREE.DataArrayTexture>; - sh3Texture: dyno.DynoUsampler2DArray<"sh3", THREE.DataArrayTexture>; - sh3TextureB: dyno.DynoUsampler2DArray<"sh3b", THREE.DataArrayTexture>; + readonly shTextures: [ + dyno.DynoUsampler2DArray<"sh1", THREE.DataArrayTexture>, + dyno.DynoUsampler2DArray<"sh2", THREE.DataArrayTexture>, + dyno.DynoUsampler2DArray<"sh3", THREE.DataArrayTexture>, + dyno.DynoUsampler2DArray<"sh3b", THREE.DataArrayTexture>, + ]; readIndex: dyno.DynoBlock< { index: "int"; numSplats: "int"; indices: "usampler2D" }, @@ -606,10 +607,10 @@ export class SplatPager { this.renderer = options.renderer; this.extSplats = options.extSplats ?? false; - this.pageSplats = 65536; + this.pageSplats = PAGE_SPLATS; this.maxSplats = options.maxSplats ?? 16777216; - this.maxPages = Math.ceil(this.maxSplats / this.pageSplats); - this.maxSplats = this.maxPages * this.pageSplats; + this.maxPages = Math.ceil(this.maxSplats / PAGE_SPLATS); + this.maxSplats = this.maxPages * PAGE_SPLATS; this.maxSh = options.maxSh ?? 3; this.curSh = 0; @@ -631,47 +632,23 @@ export class SplatPager { this.fetchPriority = []; this.packedTexture = new dyno.DynoUsampler2DArray({ - value: this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ), + value: this.newUint32ArrayTexture(4), }); this.extTexture = new dyno.DynoUsampler2DArray({ value: this.extSplats - ? this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ) + ? this.newUint32ArrayTexture(4) : SplatPager.emptyExtTexture, }); - this.sh1Texture = new dyno.DynoUsampler2DArray({ - value: this.extSplats - ? SplatPager.emptyExtSh1Texture - : SplatPager.emptySh1Texture, - }); - this.sh2Texture = new dyno.DynoUsampler2DArray({ - value: this.extSplats - ? SplatPager.emptyExtSh2Texture - : SplatPager.emptySh2Texture, - }); - this.sh3Texture = new dyno.DynoUsampler2DArray({ - value: this.extSplats - ? SplatPager.emptyExtSh3Texture - : SplatPager.emptySh3Texture, - }); - this.sh3TextureB = new dyno.DynoUsampler2DArray({ - value: SplatPager.emptyExtSh3BTexture, - }); + + const emptyShTextures = this.extSplats + ? SplatPager.emptyExtShTextures + : SplatPager.emptyShTextures; + this.shTextures = emptyShTextures.map( + (texture) => + new dyno.DynoUsampler2DArray({ + value: texture, + }), + ) as typeof this.shTextures; this.readIndex = dyno.dynoBlock( { index: "int", numSplats: "int", indices: "usampler2D" }, @@ -783,9 +760,9 @@ export class SplatPager { coord: pagedSplatTexCoord(index), viewDir, numSh, - sh1Texture: this.sh1Texture, - sh2Texture: this.sh2Texture, - sh3Texture: this.sh3Texture, + sh1Texture: this.shTextures[0], + sh2Texture: this.shTextures[1], + sh3Texture: this.shTextures[2], shMax, }).rgb; rgb = dyno.add(rgb, dyno.splitGsplat(gsplat).outputs.rgb); @@ -853,10 +830,10 @@ export class SplatPager { coord: pagedSplatTexCoord(index), viewDir, numSh, - sh1Texture: this.sh1Texture, - sh2Texture: this.sh2Texture, - sh3TextureA: this.sh3Texture, - sh3TextureB: this.sh3TextureB, + sh1Texture: this.shTextures[0], + sh2Texture: this.shTextures[1], + sh3TextureA: this.shTextures[2], + sh3TextureB: this.shTextures[3], }).rgb; rgb = dyno.add(rgb, dyno.splitGsplat(gsplat).outputs.rgb); gsplat = dyno.combineGsplat({ gsplat, rgb }); @@ -876,128 +853,29 @@ export class SplatPager { this.extTexture.value.source.data = null; } - if (!this.extSplats) { - if (this.sh1Texture.value !== SplatPager.emptySh1Texture) { - this.sh1Texture.value.dispose(); - this.sh1Texture.value.source.data = null; - } - if (this.sh2Texture.value !== SplatPager.emptySh2Texture) { - this.sh2Texture.value.dispose(); - this.sh2Texture.value.source.data = null; - } - if (this.sh3Texture.value !== SplatPager.emptySh3Texture) { - this.sh3Texture.value.dispose(); - this.sh3Texture.value.source.data = null; - } - } else { - if (this.sh1Texture.value !== SplatPager.emptyExtSh1Texture) { - this.sh1Texture.value.dispose(); - this.sh1Texture.value.source.data = null; - } - if (this.sh2Texture.value !== SplatPager.emptyExtSh2Texture) { - this.sh2Texture.value.dispose(); - this.sh2Texture.value.source.data = null; - } - if (this.sh3Texture.value !== SplatPager.emptyExtSh3Texture) { - this.sh3Texture.value.dispose(); - this.sh3Texture.value.source.data = null; - } - if (this.sh3TextureB.value !== SplatPager.emptyExtSh3BTexture) { - this.sh3TextureB.value.dispose(); - this.sh3TextureB.value.source.data = null; + const emptyShTextures = this.extSplats + ? SplatPager.emptyExtShTextures + : SplatPager.emptyShTextures; + for (let i = 0; i < emptyShTextures.length; i++) { + const texture = this.shTextures[i].value; + if (texture !== emptyShTextures[i]) { + texture.dispose(); + texture.source.data = null; } } } private ensureShTextures(numSh: number) { this.curSh = Math.max(this.curSh, numSh); - if (!this.extSplats) { - if ( - this.curSh >= 1 && - this.sh1Texture.value === SplatPager.emptySh1Texture - ) { - this.sh1Texture.value = this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 2), - 256, - 256, - this.maxPages, - THREE.RGIntegerFormat, - THREE.UnsignedIntType, - "RG32UI", - ); - } - } else { - if ( - this.curSh >= 1 && - this.sh1Texture.value === SplatPager.emptyExtSh1Texture - ) { - this.sh1Texture.value = this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ); - } - } - if ( - this.curSh >= 2 && - this.sh2Texture.value === - (!this.extSplats - ? SplatPager.emptySh2Texture - : SplatPager.emptyExtSh2Texture) - ) { - this.sh2Texture.value = this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ); - } - if (!this.extSplats) { - if ( - this.curSh >= 3 && - this.sh3Texture.value === SplatPager.emptySh3Texture - ) { - this.sh3Texture.value = this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ); - } - } else { - if (this.curSh >= 3) { - if (this.sh3Texture.value === SplatPager.emptyExtSh3Texture) { - this.sh3Texture.value = this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ); - } - if (this.sh3TextureB.value === SplatPager.emptyExtSh3BTexture) { - this.sh3TextureB.value = this.newUint32ArrayTexture( - new Uint32Array(this.maxPages * 256 * 256 * 4), - 256, - 256, - this.maxPages, - THREE.RGBAIntegerFormat, - THREE.UnsignedIntType, - "RGBA32UI", - ); - } + + const emptyShTextures = this.extSplats + ? SplatPager.emptyExtShTextures + : SplatPager.emptyShTextures; + for (let i = 0; i < this.curSh; i++) { + if (this.shTextures[i].value === emptyShTextures[i]) { + const elementsPerSplat = + this.shTextures[i].value === SplatPager.emptyUint32x2 ? 2 : 4; + this.shTextures[i].value = this.newUint32ArrayTexture(elementsPerSplat); } } } @@ -1006,10 +884,6 @@ export class SplatPager { return this.pageFreelist.shift(); } - private freePage(page: number) { - this.pageFreelist.push(page); - } - getSplatsChunk(splats: PagedSplats, chunk: number) { const chunks = this.splatsChunkToPage.get(splats); if (!chunks) { @@ -1105,197 +979,53 @@ export class SplatPager { private uploadPage( page: number, packedArray: Uint32Array, - extra: Record, + shArrays: Array, extArray?: Uint32Array, ) { - const pageBase = page * this.pageSplats; - - // const gl = this.renderer.getContext() as WebGL2RenderingContext; - - // this.renderer.state.activeTexture(gl.TEXTURE0); - // this.renderer.state.bindTexture( - // gl.TEXTURE_2D_ARRAY, - // this.getGlTexture(this.packedTexture.value), - // ); - // gl.texSubImage3D( - // gl.TEXTURE_2D_ARRAY, - // 0, - // 0, - // 0, - // page, - // 256, - // 256, - // 1, - // gl.RGBA_INTEGER, - // gl.UNSIGNED_INT, - // packedArray, - // ); - - const array = this.packedTexture.value.image.data; - array - .subarray(pageBase * 4, pageBase * 4 + packedArray.length) - .set(packedArray); - this.packedTexture.value.addLayerUpdate(page); - this.packedTexture.value.needsUpdate = true; + const pageBase = page * PAGE_SPLATS; + + uploadTextureLayer(this.packedTexture, page, pageBase * 4, packedArray); if (extArray) { - const array = this.extTexture.value.image.data; - array - .subarray(pageBase * 4, pageBase * 4 + extArray.length) - .set(extArray); - this.extTexture.value.addLayerUpdate(page); - this.extTexture.value.needsUpdate = true; + uploadTextureLayer(this.extTexture, page, pageBase * 4, extArray); } - const numSh = this.extSplats - ? extra.sh3a && extra.sh3b - ? 3 - : extra.sh2 - ? 2 - : extra.sh1 - ? 1 - : 0 - : extra.sh3 - ? 3 - : extra.sh2 - ? 2 - : extra.sh1 - ? 1 - : 0; + // In case of extSplats there can be 4 shArrays for 3 sh degrees + const numSh = Math.min(shArrays.length, 3); this.ensureShTextures(numSh); - if (!this.extSplats) { - if (this.sh1Texture.value !== SplatPager.emptySh1Texture && extra.sh1) { - // this.renderer.state.bindTexture( - // gl.TEXTURE_2D_ARRAY, - // this.getGlTexture(this.sh1Texture.value), - // ); - // gl.texSubImage3D( - // gl.TEXTURE_2D_ARRAY, - // 0, - // 0, - // 0, - // page, - // 256, - // 256, - // 1, - // gl.RG_INTEGER, - // gl.UNSIGNED_INT, - // extra.sh1 as Uint32Array, - // ); - const sh1 = extra.sh1 as Uint32Array; - const array = this.sh1Texture.value.image.data; - array.subarray(pageBase * 2, pageBase * 2 + sh1.length).set(sh1); - this.sh1Texture.value.addLayerUpdate(page); - this.sh1Texture.value.needsUpdate = true; - } - } else { - if ( - this.sh1Texture.value !== SplatPager.emptyExtSh1Texture && - extra.sh1 - ) { - const sh1 = extra.sh1 as Uint32Array; - const array = this.sh1Texture.value.image.data; - array.subarray(pageBase * 4, pageBase * 4 + sh1.length).set(sh1); - this.sh1Texture.value.addLayerUpdate(page); - this.sh1Texture.value.needsUpdate = true; - } - } - - if (this.sh2Texture.value !== SplatPager.emptySh2Texture && extra.sh2) { - // this.renderer.state.bindTexture( - // gl.TEXTURE_2D_ARRAY, - // this.getGlTexture(this.sh2Texture.value), - // ); - // gl.texSubImage3D( - // gl.TEXTURE_2D_ARRAY, - // 0, - // 0, - // 0, - // page, - // 256, - // 256, - // 1, - // gl.RGBA_INTEGER, - // gl.UNSIGNED_INT, - // extra.sh2 as Uint32Array, - // ); - const sh2 = extra.sh2 as Uint32Array; - const array = this.sh2Texture.value.image.data; - array.subarray(pageBase * 4, pageBase * 4 + sh2.length).set(sh2); - this.sh2Texture.value.addLayerUpdate(page); - this.sh2Texture.value.needsUpdate = true; - } - - if (!this.extSplats) { - if (this.sh3Texture.value !== SplatPager.emptySh3Texture && extra.sh3) { - // this.renderer.state.bindTexture( - // gl.TEXTURE_2D_ARRAY, - // this.getGlTexture(this.sh3Texture.value), - // ); - // gl.texSubImage3D( - // gl.TEXTURE_2D_ARRAY, - // 0, - // 0, - // 0, - // page, - // 256, - // 256, - // 1, - // gl.RGBA_INTEGER, - // gl.UNSIGNED_INT, - // extra.sh3 as Uint32Array, - // ); - const sh3 = extra.sh3 as Uint32Array; - const array = this.sh3Texture.value.image.data; - array.subarray(pageBase * 4, pageBase * 4 + sh3.length).set(sh3); - this.sh3Texture.value.addLayerUpdate(page); - this.sh3Texture.value.needsUpdate = true; - } - } else { - if ( - this.sh3Texture.value !== SplatPager.emptyExtSh3Texture && - extra.sh3a - ) { - const sh3a = extra.sh3a as Uint32Array; - const array = this.sh3Texture.value.image.data; - array.subarray(pageBase * 4, pageBase * 4 + sh3a.length).set(sh3a); - this.sh3Texture.value.addLayerUpdate(page); - this.sh3Texture.value.needsUpdate = true; - } - if ( - this.sh3TextureB.value !== SplatPager.emptyExtSh3BTexture && - extra.sh3b - ) { - const sh3b = extra.sh3b as Uint32Array; - const array = this.sh3TextureB.value.image.data; - array.subarray(pageBase * 4, pageBase * 4 + sh3b.length).set(sh3b); - this.sh3TextureB.value.addLayerUpdate(page); - this.sh3TextureB.value.needsUpdate = true; - } + for (let i = 0; i < shArrays.length; i++) { + const array = shArrays[i]; + const elementsPerSplat = + this.shTextures[i].value.format === THREE.RGIntegerFormat ? 2 : 4; + uploadTextureLayer( + this.shTextures[i], + page, + pageBase * elementsPerSplat, + array, + ); } - - // this.renderer.state.bindTexture(gl.TEXTURE_2D_ARRAY, null); - } - - private getGlTexture(texture: THREE.Texture): WebGLTexture { - return getGlTexture(this.renderer, texture); } private newUint32ArrayTexture( - data: Uint32Array | null, - width: number, - height: number, - depth: number, - format: THREE.AnyPixelFormat, - type: THREE.TextureDataType, - internalFormat: THREE.PixelFormatGPU, + elementsPerSplat: 2 | 4, ): THREE.DataArrayTexture { - const texture = new THREE.DataArrayTexture(data, width, height, depth); - texture.format = format; - texture.type = type; - texture.internalFormat = internalFormat; + const data = new Uint32Array( + this.maxPages * PAGE_WIDTH * PAGE_HEIGHT * elementsPerSplat, + ); + const texture = new THREE.DataArrayTexture( + data, + PAGE_WIDTH, + PAGE_HEIGHT, + this.maxPages, + ); + texture.format = + elementsPerSplat === 2 ? THREE.RGIntegerFormat : THREE.RGBAIntegerFormat; + texture.type = THREE.UnsignedIntType; + texture.internalFormat = elementsPerSplat === 2 ? "RG32UI" : "RGBA32UI"; texture.needsUpdate = true; + // Avoid initial upload of empty/null data + texture.source.dataReady = false; this.renderer.initTexture(texture); return texture; } @@ -1405,7 +1135,7 @@ export class SplatPager { splats, page, chunk, - numSplats: this.pageSplats, + numSplats: PAGE_SPLATS, }); return page; } @@ -1438,14 +1168,38 @@ export class SplatPager { lodTree: extra.lodTree as Uint32Array, }); - if (!this.extSplats) { - const packedArray = (data as PackedResult).packedArray; - this.newUploads.push({ page, numSplats, packedArray, extra }); - } else { - const extArrays = (data as ExtResult).extArrays; + if (isExtResult(data, this.extSplats)) { + const extArrays = data.extArrays; const packedArray = extArrays[0]; const extArray = extArrays[1]; - this.newUploads.push({ page, numSplats, packedArray, extArray, extra }); + const shArrays = [ + data.extra.sh1 as Uint32Array, + data.extra.sh2 as Uint32Array, + data.extra.sh3a as Uint32Array, + data.extra.sh3b as Uint32Array, + ]; + shArrays.length = shArrays.findIndex((sh) => !sh); + this.newUploads.push({ + page, + numSplats, + packedArray, + extArray, + shArrays, + }); + } else { + const packedArray = data.packedArray; + const shArrays = [ + data.extra.sh1 as Uint32Array, + data.extra.sh2 as Uint32Array, + data.extra.sh3 as Uint32Array, + ]; + shArrays.length = shArrays.findIndex((sh) => !sh); + this.newUploads.push({ + page, + numSplats, + packedArray, + shArrays, + }); } } } @@ -1456,8 +1210,8 @@ export class SplatPager { if (!upload) { break; } - const { page, numSplats, packedArray, extArray, extra } = upload; - this.uploadPage(page, packedArray, extra, extArray); + const { page, numSplats, packedArray, extArray, shArrays } = upload; + this.uploadPage(page, packedArray, shArrays, extArray); } } @@ -1514,13 +1268,39 @@ export class SplatPager { static emptyPackedTexture = this.emptyUint32x4; static emptyExtTexture = this.emptyUint32x4; - static emptySh1Texture = this.emptyUint32x2; - static emptySh2Texture = this.emptyUint32x4; - static emptySh3Texture = this.emptyUint32x4; - static emptyExtSh1Texture = this.emptyUint32x4; - static emptyExtSh2Texture = this.emptyUint32x4; - static emptyExtSh3Texture = this.emptyUint32x4; - static emptyExtSh3BTexture = this.emptyUint32x4; + static emptyShTextures = [ + this.emptyUint32x2, + this.emptyUint32x4, + this.emptyUint32x4, + ] as const; + static emptyExtShTextures = [ + this.emptyUint32x4, + this.emptyUint32x4, + this.emptyUint32x4, // SH3A + this.emptyUint32x4, // SH3B + ] as const; +} + +// Convenience function to distinguish ExtResult and PackedResult +function isExtResult( + data: ExtResult | PackedResult, + extSplats: boolean, +): data is ExtResult { + return extSplats; +} + +function uploadTextureLayer( + texture: DynoUsampler2DArray, + layer: number, + dstOffset: number, + data: Uint32Array, +) { + const array = texture.value.image.data; + array.subarray(dstOffset, dstOffset + data.length).set(data); + + texture.value.addLayerUpdate(layer); + texture.value.needsUpdate = true; + texture.value.source.dataReady = true; } function getGlTexture(