diff --git a/src/effect.ts b/src/effect.ts new file mode 100644 index 0000000..d6d7a4d --- /dev/null +++ b/src/effect.ts @@ -0,0 +1,51 @@ +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) => (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); + +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); + + 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; + + 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/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..d0b5774 100644 --- a/src/process.ts +++ b/src/process.ts @@ -1,6 +1,7 @@ import { Quat, Vec3 } from 'playcanvas'; import { Column, DataTable } from './data-table'; +import { blur } from './effect'; import { transform } from './transform'; type Translate = { @@ -18,6 +19,11 @@ type Scale = { value: number; }; +type Blur = { + kind: 'blur'; + value: number; +}; + type FilterNaN = { kind: 'filterNaN'; }; @@ -57,7 +63,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 +104,10 @@ const processDataTable = (dataTable: DataTable, processActions: ProcessAction[]) case 'scale': transform(result, Vec3.ZERO, Quat.IDENTITY, processAction.value); break; + case 'blur': { + result = blur(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..89cf752 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}`); @@ -65,4 +65,4 @@ const transform = (dataTable: DataTable, t: Vec3, r: Quat, s: number) => { }; -export { transform }; +export { transform }; \ No newline at end of file