From f59389205f08a398d2faf054697866661e8a8d05 Mon Sep 17 00:00:00 2001 From: Bill ZHANG <36790218+Lutra-Fs@users.noreply.github.com> Date: Sun, 10 Dec 2023 12:46:21 +1100 Subject: [PATCH] feat: interpolation (#214) * feat: interpolation The output data type of the simulation component was changed to enhance the visual representation of the simulation. Initially, a simple Float32Array was used. However, a more complicated output data type - an array of Float32Arrays - was introduced to better represent the density data. We also added interpolation to handle low frame rates. This modification will provide a smoother simulation display. Furthermore, loggers were improved to provide more insights about the worker creation, frame rate, and the interpolation process. The naming of temporary variables holding crucial data was also refined for a better understanding of the codebase. Lastly, the code organization was improved and clearer comments were added. BREAKING CHANGE: Output from worker now distributes the data per second instead of per each run * fix(simulation): add deps bug in `useEffect` the missing deps have been re-added to useEffect in Simulation.tsx according to ESLint Check * perf(worker): optimize cache handling in modelWorker Changed the way cache is handled in the modelWorker file. Instead of duplicating the entire cache with the 'slice' method, the change uses the cache directly because the copy will automatically handled by postMessage. This optimization reduces unnecessary resource consumption and improves efficiency. --- src/components/Simulation.tsx | 95 ++++++++++++++++++++++++++++------- src/pages/index.tsx | 2 +- src/workers/modelWorker.ts | 11 +++- 3 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/components/Simulation.tsx b/src/components/Simulation.tsx index 355fdbb..4df184b 100644 --- a/src/components/Simulation.tsx +++ b/src/components/Simulation.tsx @@ -31,7 +31,7 @@ class SimulationParams { // we can pass the parameter object directly interface Renderable { params: SimulationParams; - outputSubs: Array<(density: Float32Array) => void>; + outputSubs: Array<(density: Float32Array[]) => void>; worker: Worker; disableInteraction: boolean; } @@ -97,8 +97,6 @@ function DiffusionPlane( shaderMat.fragmentShader = applyConfigToShader(fragmentShader as string); shaderMat.side = t.DoubleSide; - // provide a dummy density field first - // TODO: until we standardise parameters a bit more we'll hardcode // an advection size of 32*32 const initDensity = new Float32Array(new Array(64 * 64).fill(0)); @@ -138,14 +136,15 @@ function DiffusionPlane( const { outputSubs, worker } = props; useEffect(() => { - outputSubs.push((density: Float32Array) => { + console.log('[renderer] [event] Creating worker'); + outputSubs.push((density: Float32Array[]) => { output(density); }); // SUBSCRIPTIONS // update the density uniforms every time // output is received - function output(data: Float32Array): void { + function output(data: Float32Array[]): void { // create a copy to prevent modifying original data data = data.slice(0); const param: Record = { @@ -153,19 +152,79 @@ function DiffusionPlane( densityRangeLow: parseFloat(renderConfig.densityRangeLow), densityRangeSize: parseFloat(renderConfig.densityRangeSize), }; - // texture float value are required to be in range [0.0, 1.0], - // so we have to convert this in js - for (let i = 0; i < data.length; i++) { - let density = Math.min(data[i], param.densityRangeHigh); - density = Math.max(density, param.densityRangeLow); - density = density / param.densityRangeSize; - data[i] = density; + + function updateTexture(data: Float32Array): void { + // texture float value is required to be in range [0.0, 1.0], + // so we have to convert this in js + for (let i = 0; i < data.length; i++) { + let density = Math.min(data[i], param.densityRangeHigh); + density = Math.max(density, param.densityRangeLow); + density = density / param.densityRangeSize; + data[i] = density; + } + const tex = new t.DataTexture(data, 64, 64, t.RedFormat, t.FloatType); + tex.needsUpdate = true; + shaderMat.uniforms.density.value = tex; + } + // calculate the fps + console.log(`[renderer] [event] Received output, fps: ${data.length}`); + if (data.length < 30) { + console.log( + `[renderer] [event] FPS is low: ${data.length}, interpolation in progress`, + ); + // interpolate based on current frame rate + // calc the interplot multiplier + const interpMul = Math.ceil((30 - 1) / data.length - 1); + console.log( + `[renderer] [event] Interpolation multiplier: ${interpMul}`, + ); + // create the interpolated data + const interpData: Float32Array[] = []; + // interpolate + for (let i = 0; i < data.length; i++) { + // start with the first original frame, then interpolate interpMul times with linear interpolation, + // then add the next original frame + console.log( + `[renderer] [event] Interpolating frame ${i + 1}/${data.length}`, + ); + interpData.push(data[i]); + if (i + 1 < data.length) { + const start = data[i]; + const end = data[i + 1]; + for (let j = 0; j < interpMul; j++) { + const interp = new Float32Array(start.length); + for (let k = 0; k < start.length; k++) { + interp[k] = + start[k] + ((end[k] - start[k]) * (j + 1)) / (interpMul + 1); + } + interpData.push(interp); + } + } + } + + console.log( + `[renderer] [event] Interpolation complete, fps: ${interpData.length}`, + ); + let i = 0; + // start the interpolation + setInterval( + () => { + if (i >= interpData.length) return; + updateTexture(interpData[i]); + i++; + }, + 1000 / (data.length * interpMul), + ); + } else { + let i = 0; + setInterval(() => { + if (i >= data.length) return; + updateTexture(data[i]); + i++; + }, 1000 / data.length); } - const tex = new t.DataTexture(data, 64, 64, t.RedFormat, t.FloatType); - tex.needsUpdate = true; - shaderMat.uniforms.density.value = tex; } - }, [shaderMat, outputSubs]); + }, [outputSubs, shaderMat.uniforms.density]); const { disableInteraction } = props; let pointMoved = false; @@ -209,8 +268,8 @@ function DiffusionPlane( worker.postMessage({ func: RunnerFunc.UPDATE_FORCE, args: { - force: forceDelta, - position: loc, + forceDelta, + loc, } satisfies UpdateForceArgs, }); }, forceInterval); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index ac4ba0f..a40f2fa 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -61,7 +61,7 @@ export default function Home(props: IndexProp): React.ReactElement { // to distribute the worker messages across different components // we utilise an observer pattern where components can subscribe // their functions to different message types - const outputSubs: Array<(density: Float32Array) => void> = useMemo( + const outputSubs: Array<(density: Float32Array[]) => void> = useMemo( () => [], [], ); diff --git a/src/workers/modelWorker.ts b/src/workers/modelWorker.ts index 3fcb976..e03783c 100644 --- a/src/workers/modelWorker.ts +++ b/src/workers/modelWorker.ts @@ -205,12 +205,21 @@ function bindCallback( event: DedicatedWorkerGlobalScope, modelService: ModelService, ): void { + const cache: Float32Array[] = []; const outputCallback = (output: Float32Array): void => { + console.log('outputCallback', output); const density = new Float32Array(output.length / 3); for (let i = 0; i < density.length; i++) { density[i] = output[i * 3]; } - event.postMessage({ type: 'output', density }); + cache.push(density); }; + setInterval(() => { + console.log('cache', cache); + if (cache.length > 0) { + event.postMessage({ type: 'output', density: cache }); + cache.splice(0, cache.length); + } + }, 1000); modelService.bindOutput(outputCallback); }