From e1c18cf753d7aca48ff9dec01f29d6bd8f9eff53 Mon Sep 17 00:00:00 2001 From: simonbethke Date: Thu, 6 Nov 2025 22:56:28 +0100 Subject: [PATCH 1/4] This change adds a blur-function that at the same time drops splats that fall below 0.01 opacity --- src/index.ts | 8 ++++++++ src/process.ts | 13 +++++++++++-- src/transform.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index d5a7de9..b5809dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -272,6 +272,7 @@ const parseArguments = () => { translate: { type: 'string', short: 't', multiple: true }, rotate: { type: 'string', short: 'r', multiple: true }, scale: { type: 'string', short: 's', multiple: true }, + blur: { type: 'string', short: 'b', multiple: true }, 'filter-nan': { type: 'boolean', short: 'N', multiple: true }, 'filter-value': { type: 'string', short: 'V', multiple: true }, 'filter-harmonics': { type: 'string', short: 'H', multiple: true }, @@ -361,6 +362,12 @@ const parseArguments = () => { value: parseNumber(t.value) }); break; + case 'blur': + current.processActions.push({ + kind: 'blur', + value: parseNumber(t.value) + }); + break; case 'filter-nan': current.processActions.push({ kind: 'filterNaN' @@ -477,6 +484,7 @@ ACTIONS (can be repeated, in any order) -t, --translate Translate splats by (x, y, z) -r, --rotate Rotate splats by Euler angles (x, y, z), in degrees -s, --scale Uniformly scale splats by factor + -b, --blur Uniformly blurs splats by factor -H, --filter-harmonics <0|1|2|3> Remove spherical harmonic bands > n -N, --filter-nan Remove Gaussians with NaN or Inf values -B, --filter-box Remove Gaussians outside box (min, max corners) diff --git a/src/process.ts b/src/process.ts index 45b0b6e..50bc951 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,7 +1,7 @@ import { Quat, Vec3 } from 'playcanvas'; import { Column, DataTable } from './data-table'; -import { transform } from './transform'; +import { transform, blur as blurData } from './transform'; type Translate = { kind: 'translate'; @@ -18,6 +18,11 @@ type Scale = { value: number; }; +type Blur = { + kind: 'blur'; + value: number; +}; + type FilterNaN = { kind: 'filterNaN'; }; @@ -57,7 +62,7 @@ type Lod = { value: number; }; -type ProcessAction = Translate | Rotate | Scale | FilterNaN | FilterByValue | FilterBands | FilterBox | FilterSphere | Param | Lod; +type ProcessAction = Translate | Rotate | Scale | FilterNaN | FilterByValue | FilterBands | FilterBox | FilterSphere | Param | Lod | Blur; const shNames = new Array(45).fill('').map((_, i) => `f_rest_${i}`); @@ -98,6 +103,10 @@ const processDataTable = (dataTable: DataTable, processActions: ProcessAction[]) case 'scale': transform(result, Vec3.ZERO, Quat.IDENTITY, processAction.value); break; + case 'blur': { + result = blurData(result, processAction.value); + break; + } case 'filterNaN': { const infOk = new Set(['opacity']); const negInfOk = new Set(['scale_0', 'scale_1', 'scale_2']); diff --git a/src/transform.ts b/src/transform.ts index 6ccf323..2c1f7f4 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -1,6 +1,6 @@ import { Mat3, Mat4, Quat, Vec3 } from 'playcanvas'; -import { DataTable } from './data-table'; +import { Column, DataTable } from './data-table'; import { RotateSH } from './utils/rotate-sh'; const shNames = new Array(45).fill('').map((_, i) => `f_rest_${i}`); @@ -64,5 +64,47 @@ const transform = (dataTable: DataTable, t: Vec3, r: Quat, s: number) => { } }; +const V_THRESHOLD = 1e-12; +const LOG_OPACITY_TARGET = -15.0; -export { transform }; +const sig = (v: number) => 1 / (1 + Math.exp(-v)); +const isig = (v: number) => -1 * Math.log(1 / v - 1); +const area = (a: number, b: number, c: number) => Math.min(a, Math.min(b, c)) * Math.max(a, Math.max(b, c)); + +const blur = (dataTable: DataTable, radius: number) => { + const hasData = ['scale_0', 'scale_1', 'scale_2', 'opacity'].every(c => dataTable.hasColumn(c)); + if (!hasData) throw new Error('Required fields for blurring missing'); + + const row: any = {}; + const indices = new Uint32Array(dataTable.numRows); + let scale_0, scale_1, scale_2, density, opa: number; + let index = 0; + + for (let i = 0; i < dataTable.numRows; ++i) { + dataTable.getRow(i, row); + + scale_0 = Math.exp(row.scale_0); + scale_1 = Math.exp(row.scale_1); + scale_2 = Math.exp(row.scale_2); + + density = area(scale_0, scale_1, scale_2); + + scale_0 += radius; + scale_1 += radius; + scale_2 += radius; + + row.scale_0 = Math.log(scale_0); + row.scale_1 = Math.log(scale_1); + row.scale_2 = Math.log(scale_2); + opa = sig(row.opacity) * density / area(scale_0, scale_1, scale_2); + row.opacity = isig(opa); + if (opa >= 0.01) indices[index++] = i; + + dataTable.setRow(i, row); + } + + return dataTable.permuteRows(indices.subarray(0, index)); +}; + + +export { transform, blur }; \ No newline at end of file From 6828f4422aba39b65a967e9b36fb314d4a10c25b Mon Sep 17 00:00:00 2001 From: simonbethke Date: Fri, 7 Nov 2025 07:25:19 +0100 Subject: [PATCH 2/4] File organization --- src/effect.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ src/process.ts | 5 +++-- src/transform.ts | 44 +------------------------------------------ 3 files changed, 53 insertions(+), 45 deletions(-) create mode 100644 src/effect.ts diff --git a/src/effect.ts b/src/effect.ts new file mode 100644 index 0000000..ec8c575 --- /dev/null +++ b/src/effect.ts @@ -0,0 +1,49 @@ +import { DataTable } from './data-table'; + +// This density function multiplies only the smallest and the greatest dimension to stay related to the +// quadratic screen-area which is what is later blended on screen. Calculating the cubic volume did not work well. +const density = (a: number, b: number, c: number) => Math.min(a, Math.min(b, c)) * Math.max(a, Math.max(b, c)); + +const sigmoid = (v: number) => 1 / (1 + Math.exp(-v)); +const invSigmoid = (v: number) => -1 * Math.log(1 / v - 1); + +const blur = (dataTable: DataTable, radius: number, cutOff: number = 0.01) => { + const hasData = ['scale_0', 'scale_1', 'scale_2', 'opacity'].every(c => dataTable.hasColumn(c)); + if (!hasData) throw new Error('Required fields for blurring missing'); + + const row: any = {}; + const indices = new Uint32Array(dataTable.numRows); + let scale_0, scale_1, scale_2, oldDensity, newOpacity: number; + let index = 0; + + for (let i = 0; i < dataTable.numRows; ++i) { + dataTable.getRow(i, row); + + scale_0 = Math.exp(row.scale_0); + scale_1 = Math.exp(row.scale_1); + scale_2 = Math.exp(row.scale_2); + + oldDensity = density(scale_0, scale_1, scale_2); + + scale_0 += radius; + scale_1 += radius; + scale_2 += radius; + + newOpacity = sigmoid(row.opacity) * oldDensity / density(scale_0, scale_1, scale_2); + + if (newOpacity >= cutOff) { + indices[index++] = i; + + row.scale_0 = Math.log(scale_0); + row.scale_1 = Math.log(scale_1); + row.scale_2 = Math.log(scale_2); + row.opacity = invSigmoid(newOpacity); + + dataTable.setRow(i, row); + } + } + + return dataTable.permuteRows(indices.subarray(0, index)); +}; + +export { blur }; \ No newline at end of file diff --git a/src/process.ts b/src/process.ts index 50bc951..d0b5774 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,7 +1,8 @@ import { Quat, Vec3 } from 'playcanvas'; import { Column, DataTable } from './data-table'; -import { transform, blur as blurData } from './transform'; +import { blur } from './effect'; +import { transform } from './transform'; type Translate = { kind: 'translate'; @@ -104,7 +105,7 @@ const processDataTable = (dataTable: DataTable, processActions: ProcessAction[]) transform(result, Vec3.ZERO, Quat.IDENTITY, processAction.value); break; case 'blur': { - result = blurData(result, processAction.value); + result = blur(result, processAction.value); break; } case 'filterNaN': { diff --git a/src/transform.ts b/src/transform.ts index 2c1f7f4..89cf752 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -64,47 +64,5 @@ const transform = (dataTable: DataTable, t: Vec3, r: Quat, s: number) => { } }; -const V_THRESHOLD = 1e-12; -const LOG_OPACITY_TARGET = -15.0; -const sig = (v: number) => 1 / (1 + Math.exp(-v)); -const isig = (v: number) => -1 * Math.log(1 / v - 1); -const area = (a: number, b: number, c: number) => Math.min(a, Math.min(b, c)) * Math.max(a, Math.max(b, c)); - -const blur = (dataTable: DataTable, radius: number) => { - const hasData = ['scale_0', 'scale_1', 'scale_2', 'opacity'].every(c => dataTable.hasColumn(c)); - if (!hasData) throw new Error('Required fields for blurring missing'); - - const row: any = {}; - const indices = new Uint32Array(dataTable.numRows); - let scale_0, scale_1, scale_2, density, opa: number; - let index = 0; - - for (let i = 0; i < dataTable.numRows; ++i) { - dataTable.getRow(i, row); - - scale_0 = Math.exp(row.scale_0); - scale_1 = Math.exp(row.scale_1); - scale_2 = Math.exp(row.scale_2); - - density = area(scale_0, scale_1, scale_2); - - scale_0 += radius; - scale_1 += radius; - scale_2 += radius; - - row.scale_0 = Math.log(scale_0); - row.scale_1 = Math.log(scale_1); - row.scale_2 = Math.log(scale_2); - opa = sig(row.opacity) * density / area(scale_0, scale_1, scale_2); - row.opacity = isig(opa); - if (opa >= 0.01) indices[index++] = i; - - dataTable.setRow(i, row); - } - - return dataTable.permuteRows(indices.subarray(0, index)); -}; - - -export { transform, blur }; \ No newline at end of file +export { transform }; \ No newline at end of file From 2ff58306a21d6459c938bf4424b51700f173f909 Mon Sep 17 00:00:00 2001 From: simonbethke Date: Fri, 7 Nov 2025 07:51:52 +0100 Subject: [PATCH 3/4] Faster (but different) operation --- src/effect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/effect.ts b/src/effect.ts index ec8c575..e997990 100644 --- a/src/effect.ts +++ b/src/effect.ts @@ -2,7 +2,7 @@ import { DataTable } from './data-table'; // This density function multiplies only the smallest and the greatest dimension to stay related to the // quadratic screen-area which is what is later blended on screen. Calculating the cubic volume did not work well. -const density = (a: number, b: number, c: number) => Math.min(a, Math.min(b, c)) * Math.max(a, Math.max(b, c)); +const density = (a: number, b: number, c: number) => (a * b + a * c + b * c) / 3; const sigmoid = (v: number) => 1 / (1 + Math.exp(-v)); const invSigmoid = (v: number) => -1 * Math.log(1 / v - 1); From 1882e366c37ed585a44a26417309034407b1bfe8 Mon Sep 17 00:00:00 2001 From: simonbethke Date: Fri, 7 Nov 2025 11:53:09 +0100 Subject: [PATCH 4/4] Improve file-size reduction --- src/effect.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/effect.ts b/src/effect.ts index e997990..d6d7a4d 100644 --- a/src/effect.ts +++ b/src/effect.ts @@ -23,23 +23,25 @@ const blur = (dataTable: DataTable, radius: number, cutOff: number = 0.01) => { scale_1 = Math.exp(row.scale_1); scale_2 = Math.exp(row.scale_2); - oldDensity = density(scale_0, scale_1, scale_2); + if ((scale_0 + scale_1 + scale_2) > 3 * radius) { + oldDensity = density(scale_0, scale_1, scale_2); - scale_0 += radius; - scale_1 += radius; - scale_2 += radius; + scale_0 += radius; + scale_1 += radius; + scale_2 += radius; - newOpacity = sigmoid(row.opacity) * oldDensity / density(scale_0, scale_1, scale_2); + newOpacity = sigmoid(row.opacity) * oldDensity / density(scale_0, scale_1, scale_2); - if (newOpacity >= cutOff) { - indices[index++] = i; + if (newOpacity >= cutOff) { + indices[index++] = i; - row.scale_0 = Math.log(scale_0); - row.scale_1 = Math.log(scale_1); - row.scale_2 = Math.log(scale_2); - row.opacity = invSigmoid(newOpacity); + row.scale_0 = Math.log(scale_0); + row.scale_1 = Math.log(scale_1); + row.scale_2 = Math.log(scale_2); + row.opacity = invSigmoid(newOpacity); - dataTable.setRow(i, row); + dataTable.setRow(i, row); + } } }