diff --git a/package.json b/package.json index 8775bbcf8..aa42cb701 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@monaco-editor/react": "^4.7.0", "@react-spring/three": "^10.0.0", "@react-three/drei": "^10.7.7", - "@react-three/fiber": "^9.5.0", + "@react-three/fiber": "^9.8.0", "@react-three/postprocessing": "^3.0.4", "@types/three": "^0.182.0", "@types/webpack": "^5.28.5", @@ -81,7 +81,7 @@ "icechunk-js": "^0.6.0", "input-otp": "^1.4.2", "lucide-react": "^0.562.0", - "next": "^16.2.7", + "next": "16.3.8", "next-themes": "^0.4.6", "pnpm": "^10.34.5", "proj4": "^2.20.9", @@ -91,7 +91,7 @@ "react-icons": "^5.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.3.0", - "three": "^0.182.0", + "three": "^0.186.0", "three-stdlib": "^2.36.0", "tunnel-rat": "^0.1.2", "vaul": "^1.1.2", diff --git a/public/bz_logo.svg b/public/bz_logo.svg new file mode 100644 index 000000000..7a9a99af7 --- /dev/null +++ b/public/bz_logo.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/GlobalStates/GlobalStore.ts b/src/GlobalStates/GlobalStore.ts index 3564642c5..b0bc75b5c 100644 --- a/src/GlobalStates/GlobalStore.ts +++ b/src/GlobalStates/GlobalStore.ts @@ -59,6 +59,7 @@ type StoreState = { useF16Textures: boolean; borderCompatible: boolean; mainTextures: THREE.DataTexture[] | THREE.Data3DTexture[] | undefined; + isWebGPU: boolean; // setters setDataShape: (dataShape: number[]) => void; @@ -102,6 +103,7 @@ type StoreState = { setClampExtremes: (clampExtremes: boolean) => void; setUseF16Textures: (useF16Textures: boolean) => void; setMainTextures: (mainTextures: THREE.DataTexture[] | THREE.Data3DTexture[] | undefined) => void; + setIsWebGPU: (isWebGPU: boolean) => void; }; const createStore = () => create((set, get) => ({ @@ -148,6 +150,7 @@ const createStore = () => create((set, get) => ({ useF16Textures: false, borderCompatible: true, mainTextures: undefined, + isWebGPU: true, // setters setDataShape: (dataShape) => set({ dataShape }), @@ -209,7 +212,8 @@ const createStore = () => create((set, get) => ({ setScalingFactors: (scalingFactors) => set({ scalingFactors }), setClampExtremes: (clampExtremes) => set({ clampExtremes }), setUseF16Textures: (useF16Textures) => set({ useF16Textures }), - setMainTextures: (mainTextures) => set({ mainTextures }) + setMainTextures: (mainTextures) => set({ mainTextures }), + setIsWebGPU: (isWebGPU) => set({ isWebGPU }), })); declare global { diff --git a/src/GlobalStates/PlotStore.ts b/src/GlobalStates/PlotStore.ts index 73963f7e7..d62c383cb 100644 --- a/src/GlobalStates/PlotStore.ts +++ b/src/GlobalStates/PlotStore.ts @@ -116,7 +116,7 @@ type PlotState ={ export const usePlotStore = create((set, get) => ({ plotType: "volume", - pointSize: 5, + pointSize: 0.1, scalePoints: false, scaleIntensity: 1, quality: 200, diff --git a/src/app/favicon.ico b/src/app/favicon.ico index b027bc556..48f18b92e 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/assets/bz.svg b/src/assets/bz.svg new file mode 100644 index 000000000..98561aaa0 --- /dev/null +++ b/src/assets/bz.svg @@ -0,0 +1,1271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/bz_logo.png b/src/assets/bz_logo.png new file mode 100644 index 000000000..4ea5e4094 Binary files /dev/null and b/src/assets/bz_logo.png differ diff --git a/src/assets/bz_logo.svg b/src/assets/bz_logo.svg new file mode 100644 index 000000000..7a9a99af7 --- /dev/null +++ b/src/assets/bz_logo.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/plots/AxisLines.tsx b/src/components/plots/AxisLines.tsx index 8c16df85e..9da9873b7 100644 --- a/src/components/plots/AxisLines.tsx +++ b/src/components/plots/AxisLines.tsx @@ -9,14 +9,14 @@ import React, {useState, useMemo} from 'react' import { useShallow } from 'zustand/shallow' import { Text } from '@react-three/drei' import { LineSegmentsGeometry } from 'three/addons/lines/LineSegmentsGeometry.js'; -import { LineSegments2 } from 'three/addons/lines/LineSegments2.js'; +import { LineSegments2 } from 'three/addons/lines/webgpu/LineSegments2.js'; import { LineMaterial } from 'three-stdlib'; import { useFrame } from '@react-three/fiber'; import { parseLoc, coarsenFlatArray, linspace } from '@/utils/HelperFuncs'; import { useCSSVariable } from '../ui'; import { useAxisIndices, useDimAxis } from '@/hooks'; -import * as THREE from 'three' - +import * as THREE from 'three/webgpu' +import { CanvasText } from './utils/CanvasText'; const AXIS_CONSTANTS = { INITIAL_RESOLUTION: 7, MAX_RESOLUTION: 20, @@ -55,24 +55,22 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli const [yResolution, setYResolution] = useState(AXIS_CONSTANTS.INITIAL_RESOLUTION) const [zResolution, setZResolution] = useState(AXIS_CONSTANTS.INITIAL_RESOLUTION) const [timeResolution, setTimeResolution] = useState(0); - const isTimeCompatible = axisDimUnits[zIdx].toLowerCase().includes('since') + const isTimeCompatible = axisDimUnits[zIdx]?.toLowerCase().includes('since') const isPC = useMemo(()=>plotType == 'point-cloud',[plotType]) const globalScale = isPC ? dataShape[2]/AXIS_CONSTANTS.PC_GLOBAL_SCALE_DIVISOR : 1 const [verboseTime, setVerboseTime] = useState(false) - const depthRatio = useMemo(()=>shape.z/shape.x*timeScale,[shape, timeScale]); + const depthRatio = useMemo(()=> dataShape[0]/dataShape[2]*timeScale, [timeScale, dataShape]); const shapeRatio = useMemo(()=>shape.y/shape.x, [shape]) const timeRatio = useMemo(()=>Math.max(shape.z/shape.x * 2, 2),[shape]); - const secondaryColor = useCSSVariable('--text-plot') //replace with needed variable - const colorHex = useMemo(()=>{ - if (!secondaryColor){return} + const {colorHex, colorString} = useMemo(()=>{ + if (!secondaryColor){return {}} const col = new THREE.Color(secondaryColor) - return col.getHex() + return {colorHex: col.getHex(), colorString: `#${col.getHexString()}`} },[secondaryColor]) - - const lineMat = useMemo(()=>new LineMaterial({color: colorHex ? colorHex : 0, linewidth: AXIS_CONSTANTS.LINE_WIDTH}),[colorHex]) + const lineMat = useMemo(()=>new THREE.Line2NodeMaterial({color: colorHex?? 0, linewidth: AXIS_CONSTANTS.LINE_WIDTH}),[colorHex]) const tickLength = AXIS_CONSTANTS.TICK_LENGTH_FACTOR*globalScale; const xLine = useMemo(()=> { @@ -105,7 +103,6 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli const length = dimLengths[isX ? 2 : 1] return Math.floor(fac * (length - 1)) } - return ( {/* Horizontal Group */} @@ -119,58 +116,56 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli && - {parseLoc(dimSlices[2]?.[getFactor(idx*xValDelta, true)] || 0,axisDimUnits[xIdx])} + > + {parseLoc(dimSlices[2]?.[getFactor(idx*xValDelta, true)] || 0,axisDimUnits[xIdx])} + ))} - {axisDimNames[xIdx]} + color={colorString} + >{axisDimNames[xIdx]} {xResolution < AXIS_CONSTANTS.MAX_RESOLUTION && - setXResolution(x=> Math.min(x+1,AXIS_CONSTANTS.MAX_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > + - } + } { xResolution > AXIS_CONSTANTS.MIN_RESOLUTION && - setXResolution(x=> Math.max(x-1,AXIS_CONSTANTS.MIN_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > - - } + } @@ -183,99 +178,92 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli && - {parseLoc(dimSlices[0]?.[(Math.floor((dimLengths[0]-1)*idx*zValDelta)+Math.floor(dimLengths[0]*animProg))%dimLengths[0]] || 0,axisDimUnits[zIdx], verboseTime, timeResolution)} + >{parseLoc(dimSlices[0]?.[(Math.floor((dimLengths[0]-1)*idx*zValDelta)+Math.floor(dimLengths[0]*animProg))%dimLengths[0]] || 0,axisDimUnits[zIdx], verboseTime, timeResolution)} ))} - {axisDimNames[zIdx]} + color={colorString} + >{axisDimNames[zIdx]} {zResolution < AXIS_CONSTANTS.MAX_RESOLUTION && - setZResolution(x=> Math.min(x+1,AXIS_CONSTANTS.MAX_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > + - } + } {zResolution > AXIS_CONSTANTS.MIN_RESOLUTION && - setZResolution(x=> Math.max(x-1,AXIS_CONSTANTS.MIN_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > - - } + } {isTimeCompatible && <> - setTimeResolution(x=> Math.min(x+1,3))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} - >⇒ - ⇒ + setVerboseTime(x => !x)} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} - >① ⮀ Ⓐ - ① ⮀ Ⓐ + 0} key={'zDownRes'} anchorX={'center'} anchorY={'top'} fontSize={AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR*globalScale} position={[-zTitleOffset*0.5, -AXIS_CONSTANTS.TITLE_FONT_SIZE_FACTOR * globalScale*2, 0]} - color={colorHex} - material-depthTest={false} + color={colorString} onClick={e=>setTimeResolution(x=> Math.max(x-1,0))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} - >⇐ + >⇐ } @@ -290,64 +278,60 @@ const CubeAxis = ({flipX, flipY, flipDown}: {flipX: boolean, flipY: boolean, fli && - {parseLoc(dimSlices[1]?.[getFactor(idx*yValDelta)] || 0,axisDimUnits[yIdx])} + >{parseLoc(dimSlices[1]?.[getFactor(idx*yValDelta)] || 0,axisDimUnits[yIdx])} ))} - {axisDimNames[yIdx]} - + {yResolution < AXIS_CONSTANTS.MAX_RESOLUTION && - setYResolution(x=> Math.min(x+1,AXIS_CONSTANTS.MAX_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > + - + } {yResolution > AXIS_CONSTANTS.MIN_RESOLUTION && - setYResolution(x=> Math.max(x-1,AXIS_CONSTANTS.MIN_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > - - + } @@ -435,13 +419,13 @@ const FlatAxis = () =>{ }, [dimLengths, analysisMode, axis]) const secondaryColor = useCSSVariable('--text-plot') //replace with needed variable - const colorHex = useMemo(()=>{ - if (!secondaryColor){return} + const {colorHex, colorString} = useMemo(()=>{ + if (!secondaryColor){return {}} const col = new THREE.Color(secondaryColor) - return col.getHex() + return {colorHex: col.getHex(), colorString: `#${col.getHexString()}`} },[secondaryColor]) - const lineMat = useMemo(()=>new LineMaterial({color: colorHex ? colorHex : 0, linewidth: FLAT_AXIS_CONSTANTS.LINE_WIDTH}),[colorHex]) + const lineMat = useMemo(()=>new THREE.Line2NodeMaterial({color: colorHex ? colorHex : 0, linewidth: FLAT_AXIS_CONSTANTS.LINE_WIDTH}),[colorHex]) const tickLength = FLAT_AXIS_CONSTANTS.TICK_LENGTH; const xLine = useMemo(()=> { const geom = new LineSegmentsGeometry().setPositions( [(-1/(swap ? shapeRatio : 1)-tickLength/2), 0, 0, (1/(swap ? shapeRatio : 1)+tickLength/2), 0, 0]); @@ -478,58 +462,54 @@ const FlatAxis = () =>{ xResolution > FLAT_AXIS_CONSTANTS.MIN_RESOLUTION && - {parseLoc(axisArrays[widthIdx][getFactor(idx*xValDelta, true)],axisUnits[widthIdx])} + >{parseLoc(axisArrays[widthIdx][getFactor(idx*xValDelta, true)],axisUnits[widthIdx])} ))} - {axisNames[widthIdx]} + color={colorString} + >{axisNames[widthIdx]} {xResolution < FLAT_AXIS_CONSTANTS.MAX_RESOLUTION && - setXResolution(x=> Math.min(x+1,FLAT_AXIS_CONSTANTS.MAX_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > + - } + } { xResolution > FLAT_AXIS_CONSTANTS.MIN_RESOLUTION && - setXResolution(x=> Math.max(x-1,FLAT_AXIS_CONSTANTS.MIN_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > - - } + } @@ -540,51 +520,48 @@ const FlatAxis = () =>{ yResolution > FLAT_AXIS_CONSTANTS.MIN_RESOLUTION && - {parseLoc(axisArrays[heightIdx][getFactor(idx*yValDelta)],axisUnits[heightIdx])} + >{parseLoc(axisArrays[heightIdx][getFactor(idx*yValDelta)],axisUnits[heightIdx])} ))} - {axisNames[heightIdx]} + color={colorString} + >{axisNames[heightIdx]} {yResolution < FLAT_AXIS_CONSTANTS.MAX_RESOLUTION && - setYResolution(x=> Math.min(x+1,FLAT_AXIS_CONSTANTS.MAX_RESOLUTION))} onPointerEnter={e=>document.body.style.cursor = 'pointer'} onPointerLeave={e=>document.body.style.cursor = 'default'} > + - } + } {yResolution > FLAT_AXIS_CONSTANTS.MIN_RESOLUTION && - setYResolution(x=> Math.max(x-1,FLAT_AXIS_CONSTANTS.MIN_RESOLUTION))} @@ -592,7 +569,7 @@ const FlatAxis = () =>{ onPointerLeave={e=>document.body.style.cursor = 'default'} > - - } + } diff --git a/src/components/plots/CountryBorders.tsx b/src/components/plots/CountryBorders.tsx index 69bd8df4c..faff17cbe 100644 --- a/src/components/plots/CountryBorders.tsx +++ b/src/components/plots/CountryBorders.tsx @@ -1,16 +1,16 @@ "use client"; -import React, {useEffect, useState, useMemo} from 'react' import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; -import * as THREE from 'three' -import { useShallow } from 'zustand/shallow'; +import { useDimAxis } from '@/hooks'; import { useFrame } from '@react-three/fiber'; -import {vertexShader, bordersFrag} from '../textures/shaders' -import { invalidate } from '@react-three/fiber'; import proj4, { Converter } from 'proj4'; -import {useDimAxis} from '@/hooks'; +import { useEffect, useMemo, useState } from 'react'; +import * as THREE from 'three/webgpu'; +import { useShallow } from 'zustand/shallow'; import { sampleCRS } from '../textures/ProjectionUtils'; +import { createBorderMaterial } from '../textures/TSL/borders'; +import { uniformUpdater } from '../textures/TSL/utils/uniformUpdater'; function toSegments(coords: [number, number][], toXYZ: (lon:number, lat:number)=>THREE.Vector3, span = 1.) { const segments: THREE.Vector3[][] = [[]]; @@ -72,8 +72,12 @@ function wrapLon(lon: number, bounds: [number, number], bypass?:boolean) { } function Borders({features}:{features: any}){ - const {xRange, yRange, plotType, borderColor, nativeCRS, destCRS } = usePlotStore(useShallow(s => s)) - const {shape, axisDimArrays, remapTexture } = useGlobalStore(useShallow(s => s)) + const { plotType, nativeCRS, destCRS } = usePlotStore(useShallow(s => ({ + plotType:s.plotType, nativeCRS:s.nativeCRS, destCRS:s.destCRS + }))) + const {axisDimArrays, remapTexture } = useGlobalStore(useShallow(s => ({ + axisDimArrays:s.axisDimArrays, remapTexture:s.remapTexture + }))) const {xArray, yArray} = useDimAxis() const [xBounds, yBounds] = useMemo(()=>{ const minX = xArray[0] @@ -83,7 +87,6 @@ function Borders({features}:{features: any}){ return [[minX, maxX] as [number, number], [minY, maxY] as [number, number]] },[axisDimArrays, xArray, yArray ]) const spherize = plotType ==='sphere' - function toXYZ(lon: number, lat: number){ const [x, y, z] = spherize ? Spherize([ -lon, lat]) @@ -100,39 +103,8 @@ function Borders({features}:{features: any}){ } },[nativeCRS, destCRS]) - const lineShaderMat = useMemo(() => { - const shapeX = (shape && shape.x > 0) ? shape.x : 1; - return new THREE.ShaderMaterial( - { - glslVersion: THREE.GLSL3, - vertexShader, - fragmentShader: bordersFrag, - uniforms:{ - xBounds: {value: new THREE.Vector2(-xRange[1],-xRange[0])}, - yBounds: {value: new THREE.Vector2(yRange[0]/shapeX, yRange[1]/shapeX)}, - borderColor: {value: new THREE.Color(borderColor)}, - trim: {value: !spherize}, - }, - defines: { - USE_APOSITION: 1 - } - } - ); - }, []) - - useEffect(()=>{ - if (lineShaderMat){ - const uniforms = lineShaderMat.uniforms - uniforms.xBounds.value = new THREE.Vector2(xRange[0], xRange[1]) - const shapeX = (shape && shape.x > 0) ? shape.x : 1; - uniforms.yBounds.value = new THREE.Vector2(yRange[0]/shapeX, yRange[1]/shapeX) - uniforms.borderColor.value = new THREE.Color(borderColor) - uniforms.trim.value = !spherize - invalidate() - } - },[xRange, yRange, borderColor, spherize, shape]) - - const lineGeometries = useMemo(() => { + const lineShaderMat = useMemo(() => createBorderMaterial(), []) + const lineGeometries:THREE.BufferGeometry[] = useMemo(() => { return features.flatMap((feature: any, i: number) => { const lines: THREE.BufferGeometry[] = []; if (feature.geometry.type === 'LineString') { @@ -189,6 +161,9 @@ function Borders({features}:{features: any}){ }); return results }, [lineGeometries, lineShaderMat, spherize]); + // --- CLEANUP --- // + useEffect(()=>()=>{ lineShaderMat.dispose() },[lineShaderMat]) + useEffect(()=>()=>{ lineGeometries.map(val => val.dispose()) },[lineGeometries]) return ( <> {lines} @@ -203,11 +178,12 @@ const CountryBorders = () => { const {dataShape, shape} = useGlobalStore(useShallow(s => ({ dataShape: s.dataShape, shape: s.shape }))) - const {zRange, plotType, showBorders, timeScale, rotateFlat, pointSize, useBorderTexture, is360Deg} = usePlotStore(useShallow(s => s)) + const {zRange, plotType, showBorders, timeScale, rotateFlat, pointSize, useBorderTexture, is360Deg} = usePlotStore(useShallow(s => ({ + zRange: s.zRange, plotType: s.plotType, showBorders: s.showBorders, timeScale: s.timeScale, + rotateFlat: s.rotateFlat, pointSize: s.pointSize, useBorderTexture: s.useBorderTexture, is360Deg: s.is360Deg + }))) const {analysisMode, analysisDim:axis} = useAnalysisStore(useShallow(s => s)) - const spherize = plotType === 'sphere'; - useFrame(({camera})=>{ if (spherize){return;} if (Math.abs(camera.rotation.z) > Math.PI/2 ){ @@ -217,7 +193,6 @@ const CountryBorders = () => { if (swapSides){setSwapSides(false)} } }) - useEffect(()=>{ fetch('./ne_110m_coastline.json') .then(res => res.json()) @@ -231,10 +206,11 @@ const CountryBorders = () => { const isPC = plotType == 'point-cloud' const isFlatMap = plotType == "flat" const timeRatio = isPC ? dataShape[0]/dataShape[2] : Math.max(dataShape[0]/dataShape[2],2) - const depthRatio = (shape && shape.x > 0) ? (shape.z / shape.x) * timeScale : 1; + const depthRatio = (shape && shape.x > 0) ? dataShape[0]/dataShape[2] * timeScale : 1; const globalScale = isPC ? dataShape[2]/500 : 1 const depthScale = isPC ? depthRatio : timeRatio/2 const aspectRatio = (shape && shape.y > 0) ? (shape.x / shape.y) : 1; + uniformUpdater(); return( { > {coastLines && } @@ -252,4 +228,5 @@ const CountryBorders = () => { ) } -export {CountryBorders} +export { CountryBorders }; + diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 6ffe6fd24..b63628653 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -1,97 +1,52 @@ -import { useEffect, useMemo, useRef } from 'react' -import * as THREE from 'three' -import { vertexShader, rayMarchFrag, orthoVertex , ddaFrag} from '@/components/textures/shaders'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { UVCube } from '@/components/plots'; +import { uniformUpdater } from '@/components/textures/TSL/utils/uniformUpdater'; +import * as u from '@/components/textures/TSL/utils/uniforms'; +import { useEffect, useMemo } from 'react'; +import * as THREE from 'three/webgpu'; import { useShallow } from 'zustand/shallow'; -import { invalidate, useFrame } from '@react-three/fiber'; -import { UVCube } from '@/components/plots' +import { createDDAMaterial, createRayMarchingMaterial } from '../textures/TSL'; +import { cmap } from '../textures/TSL/utils/uniforms'; +import { buildTransferLUT } from '../textures/TSL/utils/volumeHelpers'; +import { sampleColormap } from '../textures/colormap'; import { ColumnMeshes } from './TransectMeshes'; -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; -import { functionInjector } from '../ui/Elements/ColorAdjuster'; -interface DataCubeProps { - volTexture: THREE.Data3DTexture[] | THREE.DataTexture[] | undefined, -} - -export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { - const volTexture = usePaddedTextures(propVolTexture); - const {shape, flipY, remapTexture, remapBorders, dataShape} = useGlobalStore(useShallow(s => s)) //We have to useShallow when returning an object instead of a state. I don't fully know the logic yet - const {xRange, yRange, zRange, quality, useOrtho, useRayMarch, transparency, colorScale, - vTransferRange, vTransferScale, revTransparency} = usePlotStore(useShallow(s => s)) - const meshRef = useRef(null!); - const aspectRatio = shape.y/shape.x - const timeRatio = shape.z/shape.x; - const gridShape = useMemo(()=>{ - if (remapTexture){ - return new THREE.Vector3(remapTexture.image.width, remapTexture.image.height, dataShape[0]) - } else return new THREE.Vector3(dataShape[2], dataShape[1], dataShape[0]) - - },[remapTexture, dataShape]) - const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - modelViewMatrixInverse: { value: new THREE.Matrix4() }, // Used for Orthographic RayMarcher - map: { value: volTexture}, - dataShape: {value: gridShape}, - remapTexture: { value: remapTexture?? remapBorders}, - scale: {value: shape}, - flatBounds:{value: new THREE.Vector4(-xRange[1],-xRange[0],zRange[0] * timeRatio, zRange[1] * timeRatio)}, - vertBounds:{value: flipY - ? new THREE.Vector2(yRange[0]*aspectRatio,yRange[1]*aspectRatio) - : new THREE.Vector2(yRange[0]*aspectRatio,yRange[1]*aspectRatio) - }, - steps: { value: quality }, - transparency: {value: transparency}, - revTransparency: {value: revTransparency}, - opacityMag: {value: vTransferScale}, - useClipScale: {value: vTransferRange}, - ...uniforms - }, - defines: { - USE_VORIGIN: 1, - USE_VDIRECTION: 1, - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: useOrtho ? orthoVertex : vertexShader, - fragmentShader: useRayMarch ? functionInjector(rayMarchFrag, colorScale): functionInjector(ddaFrag, colorScale), - transparent: true, - blending: THREE.NormalBlending, - depthWrite: false, - side: useOrtho ? THREE.FrontSide : THREE.BackSide, - }),[useRayMarch, useOrtho, volTexture, colorScale, remapTexture]); - - const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); - updateCommonUniforms(shaderMaterial) - useEffect(() => { - if (shaderMaterial) { - const uniforms = shaderMaterial.uniforms - uniforms.dataShape.value = gridShape; - uniforms.scale.value = shape; - uniforms.flatBounds.value.set(-xRange[1], -xRange[0], zRange[0] * timeRatio, zRange[1] * timeRatio); - flipY ? uniforms.vertBounds.value.set(-yRange[1] * aspectRatio, -yRange[0] * aspectRatio) - : uniforms.vertBounds.value.set(yRange[0] * aspectRatio, yRange[1] * aspectRatio); - uniforms.steps.value = quality; - uniforms.transparency.value = transparency; - uniforms.revTransparency.value = revTransparency; - uniforms.opacityMag.value = vTransferScale; - uniforms.useClipScale.value = vTransferRange; - invalidate() // Needed because Won't trigger re-render if camera is stationary. - } - }, [shape, gridShape, xRange, yRange, zRange, aspectRatio, quality, transparency, revTransparency, vTransferScale, vTransferRange, flipY]); - useFrame(({camera})=>{ // This calculates InverseModel matrix for the orthographic raymarcher - if (!useOrtho || !meshRef.current || !shaderMaterial) return; - meshRef.current.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, meshRef.current.matrixWorld); - shaderMaterial.uniforms.modelViewMatrixInverse.value - .copy(meshRef.current.modelViewMatrix) - .invert(); - }) - return ( - - - - - - ) +export const DataCube = ( ) => { + const {shape, flipY, remapTexture} = useGlobalStore(useShallow(s => ({ + shape:s.shape, flipY:s.flipY, remapTexture:s.remapTexture + }))) //We have to useShallow when returning an object instead of a state. I don't fully know the logic yet + const { useOrtho, valueRange, useRayMarch, transparency, + vTransferScale, revTransparency, interpPixels, colorScale} = usePlotStore(useShallow(s => ({ + useOrtho:s.useOrtho, valueRange:s.valueRange, useRayMarch:s.useRayMarch, + transparency:s.transparency, vTransferScale:s.vTransferScale, revTransparency:s.revTransparency, interpPixels:s.interpPixels, colorScale: s.colorScale + }))) + const shaderMaterial = useMemo(() => useRayMarch + ? createRayMarchingMaterial(useOrtho) + : createDDAMaterial(useOrtho) + , [useRayMarch, remapTexture, useOrtho, interpPixels, colorScale]); + const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); + const cmapFunc = (val: number) => sampleColormap(val, cmap.value as THREE.DataTexture) + // --- GENERATE LUT --- // + useEffect(()=>{ + const newLUT = buildTransferLUT({ + cmap:cmapFunc, + threshold:valueRange as [number, number], + revTransparency, + transparency, + opacityMag:vTransferScale, + useClipScale:false + }) + u.tfLUT.value = newLUT; + },[valueRange, revTransparency, transparency, vTransferScale, cmap.value]) + uniformUpdater(); + // --- CLEANUP --- // + useEffect(()=>()=>{ shaderMaterial.dispose() },[shaderMaterial]) + useEffect(()=>()=>{ geometry.dispose() },[geometry]) + return ( + + + + + + ) } \ No newline at end of file diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index 6f18fbe06..210c6a3ba 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -1,118 +1,76 @@ -import React, { useEffect, useMemo } from 'react' +import * as u from '@/components/textures/TSL/utils/uniforms'; +import { uniformUpdater } from '@/components/textures/TSL/utils/uniformUpdater'; import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; +import { useErrorStore } from '@/GlobalStates/ErrorStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; -import { useErrorStore } from '@/GlobalStates/ErrorStore'; -import { useShallow } from 'zustand/shallow' -import * as THREE from 'three' -import { flatBlocksVert, sphereBlocksFrag } from '../textures/shaders' -import { invalidate } from '@react-three/fiber' -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { useDimAxis, useValueScales } from '@/hooks'; -import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; -import { functionInjector } from '../ui/Elements/ColorAdjuster'; - -const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | undefined}) => { - const textures = usePaddedTextures(propTextures); - const {isFlat, flipY, dataShape, axisDimArrays, remapTexture, remapBorders} = useGlobalStore(useShallow(s => ({ - isFlat: s.isFlat, flipY: s.flipY, dataShape: s.dataShape, axisDimArrays: s.axisDimArrays, - remapTexture: s.remapTexture, remapBorders: s.remapBorders - }))) - const { displacement, offsetNegatives, rotateFlat, colorScale} = usePlotStore(useShallow(s => ({ - displacement: s.displacement, offsetNegatives: s.offsetNegatives, rotateFlat: s.rotateFlat, colorScale: s.colorScale +import { useDimAxis } from '@/hooks'; +import { useMemo, useEffect } from 'react'; +import * as THREE from 'three/webgpu'; +import { useShallow } from 'zustand/shallow'; +import { createFlatBlocksMaterial } from '../textures/TSL/flatBlocks'; +const FlatBlocks = () => { + const {flipY, dataShape, axisDimArrays, remapTexture} = useGlobalStore(useShallow(s => ({ + flipY: s.flipY, dataShape: s.dataShape, axisDimArrays: s.axisDimArrays, + remapTexture: s.remapTexture, }))) + const {rotateFlat, colorScale} = usePlotStore(useShallow(s => ({rotateFlat: s.rotateFlat, colorScale: s.colorScale}))) const {analysisMode, analysisDim:axis} = useAnalysisStore(useShallow(s => s)) - const valueScales = useValueScales(); const {xArray, yArray} = useDimAxis() const {width, height} = useMemo(()=>{ - if (analysisMode){ + const [width, height] = remapTexture + ? [remapTexture.image.width, remapTexture.image.height] + : [xArray.length, yArray.length] + if (analysisMode && axis != 0){ const thisShape = dataShape.filter((_val, idx) => idx != axis) + u.displacementResolution.value = new THREE.Vector2(thisShape[1], thisShape[0]) return {width: thisShape[1], height: thisShape[0]} } else { - return {width: xArray.length, height: yArray.length} + u.displacementResolution.value = new THREE.Vector2(width, height) + return {width, height} } - },[analysisMode, axis, dataShape,xArray, yArray, axisDimArrays]) + },[analysisMode, axis, dataShape,xArray, yArray, axisDimArrays, remapTexture]) const rotateMap = analysisMode && axis == 2; const count = useMemo(()=>{ - const count = width * height; - if (count * 16 *4 > 2e9){ - useErrorStore.setState({ error:'largeArray' }) - return 0 - } - return count - },[width, height]) - const geometry = useMemo(()=>{ const count = width * height; if (count * 16 *4 > 2e9){ - return undefined - } - const sqWidth = 2; - const aspect = width/height - const geo = new THREE.BoxGeometry(sqWidth/width, sqWidth/height/aspect, .01); - const uvs = new Float32Array(count * 2); - let idx = 0; - for (let i = 0; i < width; i++) { - for (let j = 0; j < height; j++) { - const u = (i + 0.5) / width; - const v = (j + 0.5) / height; - uvs[idx * 2] = u; - uvs[idx * 2 + 1] = v; - idx ++; - } + useErrorStore.setState({ error:'largeArray' }) + return 0 } - geo.setAttribute( - 'instanceUV', - new THREE.InstancedBufferAttribute(uvs, 2) - ); - return geo + u.displacementResolution.value = new THREE.Vector2(width,height); + return count },[width, height]) - const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=>{ - const shader = new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - map: { value: textures }, - remapTexture: { value: remapTexture?? remapBorders}, - aspect: {value: width/height}, - displaceZero: {value: offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) }, - displacement: {value: displacement}, - ...uniforms - }, - defines:{ - ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: functionInjector(flatBlocksVert, colorScale), - fragmentShader: functionInjector(sphereBlocksFrag, colorScale), - blending: THREE.NoBlending, - depthWrite:true, - depthTest:true, - }) - return shader - },[isFlat, remapTexture, colorScale]) - - updateCommonUniforms(shaderMaterial); - useEffect(()=>{ - if (shaderMaterial){ - const uniforms = shaderMaterial.uniforms; - uniforms.map.value = textures; - uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) - uniforms.displacement.value = displacement - uniforms.displaceZero.value = offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) - uniforms.aspect.value = width/height; - } - invalidate(); - },[valueScales, displacement, offsetNegatives, textures, width, height]) - + const geometry = useMemo(()=>{ + const sqWidth = 2; + const aspect = width/height + const boxHeight = 0.05; + const geo = new THREE.BoxGeometry(sqWidth/width, sqWidth/height/aspect, boxHeight); + geo.translate(0, 0, boxHeight / 2,); + return geo + },[width, height]) + const material = useMemo(()=>{ + const m = createFlatBlocksMaterial(count); + m.depthWrite = true; + m.depthTest = true; + return m; + },[count, colorScale]) + const mesh = useMemo(()=>{ + const newMesh = new THREE.Mesh(geometry, material) + newMesh.count = count; + newMesh.frustumCulled = false; + return newMesh + },[geometry, material, count]) + // --- UNIFORMS --- // + uniformUpdater(); + // --- CLEANUP --- // + useEffect(()=>()=>{ material.dispose() },[material]) + useEffect(()=>()=>{ geometry.dispose() },[geometry]) return ( - - + + + ) } -export {FlatBlocks} +export { FlatBlocks }; + diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index af68fb087..ab8934d62 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -1,38 +1,33 @@ "use client"; -import React, {useMemo, useEffect, useState, useRef} from 'react' -import * as THREE from 'three' import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; -import { vertShader } from '@/components/textures/shaders' -import { useShallow } from 'zustand/shallow' -import { ThreeEvent } from '@react-three/fiber'; +import { evaluateColorMap } from '@/components/textures'; +import { useAxisIndices, useDimAxis } from '@/hooks'; +import { uniformUpdater } from '@/components/textures/TSL/utils/uniformUpdater'; import { GetCurrentArray, GetTimeSeries, parseUVCoords } from '@/utils/HelperFuncs'; +import { ThreeEvent } from '@react-three/fiber'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import * as THREE from 'three/webgpu'; +import { useShallow } from 'zustand/shallow'; import { sampleCRS } from '../textures/ProjectionUtils'; -import { evaluateColorMap } from '@/components/textures'; -import { flatFrag } from '../textures/shaders'; +import { createFlatMapMaterial } from '../textures/TSL/flatMap'; +import { InfoViewer } from '../ui/Elements/InfoViewer'; import { SquareMeshes } from './TransectMeshes'; -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { useAxisIndices, useDimAxis } from '@/hooks'; -import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; -import { functionInjector } from '../ui/Elements/ColorAdjuster'; -import {InfoViewer} from '../ui/Elements/InfoViewer'; - -const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[]}) => { +import * as u from '@/components/textures/TSL/utils/uniforms' +const FlatMap = () => { // ---- Imports ---- // - const textures = usePaddedTextures(propTextures); const {flipY, dimNames, dimUnits, isFlat, variable2, - dataShape, strides, remapTexture, remapBorders, shape, + dataShape, strides, remapTexture, shape, bivariate, setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(s => ({ flipY:s.flipY, dimNames:s.dimNames, dimUnits:s.dimUnits, isFlat:s.isFlat, dataShape:s.dataShape, strides:s.strides, bivariate: s.bivariate, - remapTexture:s.remapTexture, remapBorders:s.remapBorders, shape:s.shape, variable2: s.variable2, + remapTexture:s.remapTexture, shape:s.shape, variable2: s.variable2, setPlotDim:s.setPlotDim, updateDimCoords:s.updateDimCoords, updateTimeSeries:s.updateTimeSeries }))) - const {animProg, selectTS, colorScale, - getColorIdx, incrementColorIdx} = usePlotStore(useShallow(s => ({ - animProg:s.animProg, selectTS:s.selectTS, colorScale:s.colorScale, + const {animProg, selectTS, colorScale, interpPixels, getColorIdx, incrementColorIdx} = usePlotStore(useShallow(s => ({ + animProg:s.animProg, selectTS:s.selectTS, colorScale: s.colorScale, interpPixels: s.interpPixels, getColorIdx:s.getColorIdx, incrementColorIdx:s.incrementColorIdx }))) const {analysisDim:axis, analysisMode, analysisArray} = useAnalysisStore(useShallow(s => s)) @@ -63,10 +58,6 @@ const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | TH // --- Geometry --- // const geometry = useMemo(()=>new THREE.PlaneGeometry(2,2*shapeRatio),[shapeRatio]) const rotateMap = analysisMode && axis == 2; - useEffect(()=>{ - geometry.dispose() - },[geometry]) - // ----- Info Viewer----- // const [loc, setLoc] = useState<[number, number]>([0,0]); const [showInfo, setShowInfo] = useState(false); @@ -95,7 +86,7 @@ const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | TH bivariate ? [GetCurrentArray(), GetCurrentArray(undefined, variable2)] : [GetCurrentArray()], - [analysisMode, analysisArray, textures, bivariate, variable2]) + [analysisMode, analysisArray, bivariate, variable2]) const handleMove = (e: ThreeEvent) => { if (e.uv) { @@ -174,30 +165,17 @@ const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | TH } updateDimCoords({[tsID] : dimObj}) } - // ----- SHADER MATERIAL ----- // - const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms:{ - map: {value: textures}, - remapTexture: {value: remapTexture?? remapBorders}, - ...uniforms - }, - defines:{ - ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: vertShader, - fragmentShader: functionInjector(flatFrag, colorScale), - side: THREE.DoubleSide, - }),[isFlat, textures, remapTexture, colorScale]) - updateCommonUniforms(shaderMaterial) + uniformUpdater(); + const shaderMaterial = useMemo(() => createFlatMapMaterial(), [colorScale, interpPixels]) ; useEffect(()=>{ // This is duplicated. Probably shoud just move it to Plot.tsx useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) },[remapTexture]) + // --- CLEANUP --- // + useEffect(()=>()=>{ shaderMaterial.dispose() },[shaderMaterial]) + useEffect(()=>()=>{ geometry.dispose() },[geometry]) return ( <> @@ -221,4 +199,5 @@ const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | TH ) } -export {FlatMap} +export { FlatMap }; + diff --git a/src/components/plots/MorphingPoints.tsx b/src/components/plots/MorphingPoints.tsx index a08637673..ae396ffac 100644 --- a/src/components/plots/MorphingPoints.tsx +++ b/src/components/plots/MorphingPoints.tsx @@ -1,227 +1,75 @@ "use client"; - -import React, { useMemo, useRef, useEffect } from 'react'; -import * as THREE from 'three'; -import { Canvas, extend, useFrame, useThree } from '@react-three/fiber'; -import gsap from 'gsap'; -import vertexShader from '@/components/textures/shaders/LandingVertex.glsl' -import fragmentShader from '@/components/textures/shaders/LandingFrag.glsl' -import './Plots.css'; +import { useMemo } from 'react'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { Canvas } from '@react-three/fiber'; +import * as THREE from 'three/webgpu'; import { useShallow } from 'zustand/shallow'; -import { useColormapStore } from '@/GlobalStates/ColormapStore'; - - -// Define the type for our custom shader material's uniforms -type MorphMaterialType = THREE.ShaderMaterial & { - uniforms: { - uSphereMix: { value: number }; - uCubeMix: { value: number }; - uPlaneMix: { value: number }; - uSize: { value: number }; - uTime: { value: number }; - }; -}; - -// Define the shader material using drei's helper - - -// Make the material available to extend - - +import { createMorphingMaterial } from '../textures/TSL/morphingPoints'; +import './Plots.css'; +import { uniformUpdater } from '../textures/TSL/utils/uniformUpdater'; +import { useEffect } from 'react'; +import { pointerPos } from '../textures/TSL/morphingPoints'; +import { WebGPUPlacard } from '../ui/Elements/WebGPUPlacard'; const MorphingPoints = () => { - const pointsRef = useRef(null); const count = 15625; // Total number of points - const {gl} = useThree(); - const { setMaxTextureSize, setMax3DTextureSize } = usePlotStore(useShallow(s => ({ - setMaxTextureSize: s.setMaxTextureSize, setMax3DTextureSize:s.setMax3DTextureSize - }))) - - useEffect(()=>{ - const context = gl.getContext() - //@ts-expect-error This parameter does exist - setMax3DTextureSize(context.getParameter(context.MAX_3D_TEXTURE_SIZE)) - setMaxTextureSize(context.getParameter(context.MAX_TEXTURE_SIZE)) - },[]) - - const colormap = useColormapStore(s => s.colormap) - // Pre-calculate the point positions for each shape using useMemo for performance - const { spherePositions, cubePositions, planePositions } = useMemo(() => { - const spherePositions = new Float32Array(count * 3); - const cubePositions = new Float32Array(count * 3); - const planePositions = new Float32Array(count * 3); - - // --- Sphere Positions (using Fibonacci lattice for even distribution) --- - const phi = Math.PI * (3.0 - Math.sqrt(5.0)); // Golden angle - for (let i = 0; i < count; i++) { - const y = 1 - (i / (count - 1)) * 2; // y goes from 1 to -1 - const radius = Math.sqrt(1 - y * y); - const theta = phi * i; - - const x = Math.cos(theta) * radius; - const z = Math.sin(theta) * radius; - - spherePositions[i * 3] = x * 1.2; - spherePositions[i * 3 + 1] = y * 1.2; - spherePositions[i * 3 + 2] = z * 1.2; - } - - // --- Cube Positions (16x16x16 grid) --- - const cubRes = 25 - let i = 0; - for (let x = 0; x < cubRes; x++) { - for (let y = 0; y < cubRes; y++) { - for (let z = 0; z < cubRes; z++) { - cubePositions[i * 3] = (x / cubRes - 0.5) * 2; - cubePositions[i * 3 + 1] = (y / cubRes - 0.5) * 2; - cubePositions[i * 3 + 2] = (z / cubRes - 0.5) * 2; - i++; - } - } - } - - // --- Plane Positions (64x64 grid) --- - const planeRes = 125 - i = 0; - for (let x = 0; x < planeRes; x++) { - for (let y = 0; y < planeRes; y++) { - planePositions[i * 3] = (x / planeRes - 0.5) * 2.5 ; - planePositions[i * 3 + 1] = (y / planeRes - 0.5) * 2.5 ; - planePositions[i * 3 + 2] = 0; - i++; - } - } - - return { spherePositions, cubePositions, planePositions }; - }, [count]); - - const MorphMaterial = useMemo(()=>new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - uSphereMix: {value: 1.0}, - uCubeMix: {value: 0.0}, - uPlaneMix: {value: 0.0}, - uTime: {value: 0.0}, - cmap: { value: colormap} - }, - vertexShader, - fragmentShader - }),[]) - // Animation effect - useEffect(() => { - let tl: gsap.core.Timeline | null = null; - - if (MorphMaterial) { - const uniforms = MorphMaterial.uniforms; - - // Create a GSAP timeline for the morphing animation - tl = gsap.timeline({ - repeat: -1, // Loop indefinitely - yoyo: false, - }); - - const duration = 2; // Duration of each morph transition - const delay = 3; // Time to hold each shape before morphing - - // 1. Morph from Sphere to Cube - tl.to(uniforms.uCubeMix, { - value: 1, - duration, - delay, - ease: 'power2.inOut', - }); - - // 2. Morph from Cube to Plane - tl.to(uniforms.uCubeMix, { - value: 0, - duration, - delay, - ease: 'power2.inOut', - }); - tl.to(uniforms.uPlaneMix, { - value: 1, - duration, - ease: 'power2.inOut', - }, "<"); // Animate at the same time as the previous tween - - // 3. Morph from Plane back to Sphere - tl.to(uniforms.uPlaneMix, { - value: 0, - duration, - delay, - ease: 'power2.inOut', - }); - } - // Cleanup function - always return this - return () => { - if (tl) { - tl.kill(); - } - }; - }, []); - - // Update time uniform for the dynamic wave animation in the shader - useFrame((state) => { - if(MorphMaterial){ - MorphMaterial.uniforms.uTime.value = state.clock.getElapsedTime(); - } - if (pointsRef.current) { - pointsRef.current.rotation.y += 0.001; // Slow rotation around Y-axis - pointsRef.current.rotation.x += 0.001; - } - }); - - useEffect(()=>{ - if(MorphMaterial){ - MorphMaterial.uniforms.cmap.value = colormap - } - },[colormap]) - + const material = useMemo(()=>createMorphingMaterial(count), [count]) return ( - - - {/* The 'position' attribute is not used by the shader for final position, - but it's good practice to have it. We'll use sphere positions as the base. */} - - - - - - {/* Use the custom morphMaterial */} - + ); }; - export const LandingShapes = () =>{ - return( -
- {/*
- Browzarr -
*/} - - - - -
- ) + const setIsWebGPU = useGlobalStore(s => s.setIsWebGPU) + const { setMaxTextureSize, setMax3DTextureSize } = usePlotStore(useShallow(s => ({ + setMaxTextureSize: s.setMaxTextureSize, setMax3DTextureSize:s.setMax3DTextureSize + }))) + useEffect(()=>{ + async function getLimits(){ + const adapter = await navigator.gpu.requestAdapter() + if (adapter){ + const max2DSize = adapter.limits.maxTextureDimension2D; + const max3DSize = adapter.limits.maxTextureDimension3D; + setIsWebGPU(true) + setMaxTextureSize(max2DSize); setMax3DTextureSize(max3DSize) + return + } else{ + setIsWebGPU(false) + const canvas = document.createElement('canvas') + let gl: WebGL2RenderingContext | WebGLRenderingContext | null = canvas.getContext('webgl2') + if (!gl) gl = canvas.getContext('webgl') + const max2DSize = gl?.getParameter(gl.MAX_TEXTURE_SIZE) + const max3DSize = gl instanceof WebGL2RenderingContext + ? gl.getParameter(gl.MAX_3D_TEXTURE_SIZE) + : 0 + setMaxTextureSize(max2DSize); setMax3DTextureSize(max3DSize) + return + } + } + getLimits() + }, []) + uniformUpdater(); + return( +
+ + { + const renderer = new THREE.WebGPURenderer(props as any) + await renderer.init() + return renderer + }} + > + + pointerPos.value.set(e.point.x, e.point.y)} + > + + + + +
+ ) } \ No newline at end of file diff --git a/src/components/plots/Plot.tsx b/src/components/plots/Plot.tsx index 3b3fd869c..cfe98ad78 100644 --- a/src/components/plots/Plot.tsx +++ b/src/components/plots/Plot.tsx @@ -1,6 +1,6 @@ -import { OrbitControls, useTexture } from '@react-three/drei'; -import React, { useMemo, useRef, useState, useEffect } from 'react'; -import * as THREE from 'three'; +import { OrbitControls } from '@react-three/drei'; +import React, { useMemo, useRef, useEffect } from 'react'; +import * as THREE from 'three/webgpu'; import { PointCloud, DataCube, FlatMap, Sphere, CountryBorders, AxisLines, SphereBlocks, FlatBlocks, KeyFramePreviewer } from '@/components/plots'; import { Canvas, invalidate, useThree } from '@react-three/fiber'; import { ArrayToTexture, createDataTexture } from '@/components/textures'; @@ -16,8 +16,7 @@ import ExportCanvas from '@/utils/ExportCanvas'; import { useDataFetcher } from '@/hooks/useDataFetcher'; import { reproject } from '@/components/textures/ProjectionTexture'; import { GetCurrentArray } from '@/utils/HelperFuncs'; -import { useColormapStore } from '@/GlobalStates/ColormapStore'; - +import * as u from '@/components/textures/TSL/utils/uniforms' const TransectNotice = () =>{ const selectTS = usePlotStore(s => s.selectTS) return ( @@ -28,7 +27,6 @@ const TransectNotice = () =>{ ) } - const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ const {resetCamera, useOrtho, displaceFaces, cameraPosition, overRideCamera} = usePlotStore(useShallow(s => ({ resetCamera: s.resetCamera, useOrtho: s.useOrtho, displaceFaces: s.displaceFaces, @@ -155,11 +153,10 @@ const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ const MemoOrbit = React.memo(Orbiter) const Plot = () => { - const {isFlat, DPR, mainTextures, dataShape, useF16Textures, setMainTextures, setIsFlat, setStatus } = useGlobalStore(useShallow(s =>({ - isFlat: s.isFlat, DPR: s.DPR, mainTextures: s.mainTextures, dataShape: s.dataShape, useF16Textures: s.useF16Textures, - setMainTextures: s.setMainTextures, setIsFlat: s.setIsFlat, setStatus: s.setStatus + const {isFlat, DPR, dataShape, useF16Textures, setMainTextures, setIsFlat, setStatus, flipY } = useGlobalStore(useShallow(s =>({ + isFlat: s.isFlat, DPR: s.DPR, dataShape: s.dataShape, useF16Textures: s.useF16Textures, + setMainTextures: s.setMainTextures, setIsFlat: s.setIsFlat, setStatus: s.setStatus, flipY: s.flipY }))) - const colormap = useColormapStore(s => s.colormap) const keyFrameEditor = useImageExportStore(s => s.keyFrameEditor) const {plotType, displaceFaces, setPlotType} = usePlotStore(useShallow(s => ({ plotType: s.plotType, displaceFaces: s.displaceFaces, setPlotType: s.setPlotType @@ -167,10 +164,6 @@ const Plot = () => { const {analysisMode, analysisArray, useEditor} = useAnalysisStore(useShallow(s => ({ analysisMode: s.analysisMode, analysisArray: s.analysisArray, useEditor: s.useEditor }))) - const coords = useRef([0,0]) - const val = useRef(0) - const [showInfo, setShowInfo] = useState(false) - const [loc, setLoc] = useState([0,0]) //DATA LOADING const {show, stableMetadata} = useDataFetcher() @@ -202,12 +195,6 @@ const Plot = () => { } setStatus(null) },[useF16Textures]) - const infoSetters = useMemo(()=>({ - setLoc, - setShowInfo, - coords, - val - }),[]) const prevUniqueRep = useRef(false); ` Point cloud and sphere need unique reprojections. However, reprojecting from volume @@ -228,12 +215,13 @@ const Plot = () => { const borderTexture = await loader.loadAsync('./border_distance_sdf.png'); borderTexture.magFilter = THREE.LinearFilter; borderTexture.minFilter = THREE.LinearFilter; - borderTexture.needsUpdate = true; - usePlotStore.setState({borderTexture, maskTexture}); + u.borderTexture.value = borderTexture; + u.maskTexture.value = maskTexture; + u.borderTexture.value.needsUpdate = true; + u.maskTexture.value.needsUpdate = true; } SetTextures() },[]) - const Nav = useMemo(()=>Navbar,[]) return (
{
) } diff --git a/src/components/plots/PointCloud.tsx b/src/components/plots/PointCloud.tsx index 13e43b796..1c7be8f3b 100644 --- a/src/components/plots/PointCloud.tsx +++ b/src/components/plots/PointCloud.tsx @@ -1,32 +1,21 @@ -import * as THREE from 'three' -import { useEffect, useMemo } from 'react' -import { pointFrag, pointVert } from '@/components/textures/shaders' +import * as u from '@/components/textures/TSL/utils/uniforms'; +import { uniformUpdater } from '@/components/textures/TSL/utils/uniformUpdater'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { useEffect, useMemo } from 'react'; +import * as THREE from 'three/webgpu'; import { useShallow } from 'zustand/shallow'; -import { UVCube } from './UVCube'; +import { createPointMaterial } from '../textures/TSL/points'; import { ColumnMeshes } from './TransectMeshes'; - -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; -import { functionInjector } from '../ui/Elements/ColorAdjuster'; - -interface PCProps { - texture: THREE.Data3DTexture[] | null, - colormap: THREE.DataTexture -} +import { UVCube } from './UVCube'; const MappingCube = () =>{ - const {dataShape, shape} = useGlobalStore(useShallow(s => s)) - const {timeScale} = usePlotStore(useShallow(s => s)) - - const globalScale = dataShape[2]/500 - const offset = 1/500; //I don't really understand that. But the cube is off by one pixel in each dimension - - const depthRatio = useMemo(()=> (shape && shape.x > 0 ? (shape.z / shape.x) * timeScale : 1),[shape, timeScale]); + const timeScale = usePlotStore(s => s.timeScale) + const globalScale = dataShape[2]/100 + const offset = 1/100; //I don't really understand that. But the cube is off by one pixel in each dimension + const depthRatio = useMemo(()=> (shape && shape.x > 0 ? (shape.z / shape.x) * timeScale * 2: 1),[shape, timeScale]); const shapeRatio = useMemo(()=>dataShape[1]/dataShape[2], [dataShape]) - return( @@ -34,110 +23,61 @@ const MappingCube = () =>{ ) } -export const PointCloud = ({textures} : {textures:PCProps} )=>{ - const volTexture = usePaddedTextures(textures.texture); - const { flipY, dataShape, remapTexture, shape, remapBorders } = useGlobalStore(useShallow(s => s)) - const {scalePoints, scaleIntensity, pointSize, valueRange, colorScale, - timeScale, xRange, yRange, zRange, disablePointScale} = usePlotStore(useShallow(s => s)) - - //Extract data and shape from Data3DTexture - const { width, height, depth } = useMemo(() => { - const [depth, height, width] = dataShape - return { - width: width, - height: height, - depth: depth, - }; - }, [dataShape]); - const globalscale = dataShape[2]/500 - const depthRatio = useMemo(()=> (shape && shape.x > 0 ? (shape.z / shape.x) * timeScale : 1),[shape, timeScale]); - - const geometries = useMemo(() => { - const numPoints = depth * height * width; - const maxPoints = 1e8; - const stride = numPoints > maxPoints ? Math.ceil(numPoints / maxPoints) : 1; - - const subNumPoints = Math.floor(numPoints / stride); - const indexData = new Int32Array(subNumPoints); - - let writePtr = 0; - for (let i = 0; i < numPoints; i += stride) { - if (writePtr < subNumPoints) { - indexData[writePtr++] = i; - } +export const PointCloud = ( )=>{ + const { flipY, dataShape, remapTexture, shape, textureData, bivariate } = useGlobalStore(useShallow(s => ({ + flipY: s.flipY, dataShape: s.dataShape, remapTexture: s.remapTexture, shape: s.shape, + textureData: s.textureData, bivariate: s.bivariate + }))) + const { colorScale, xRange, yRange, zRange} = usePlotStore(useShallow(s => ({ + colorScale: s.colorScale, xRange: s.xRange, yRange: s.yRange, zRange: s.zRange + }))) + // --- INITIALIZE --- // + const globalscale = dataShape[2]/100 + const count = dataShape.reduce((a, b) => a * b , 1) + const materialObjects = useMemo(() => { + const maxPoints = 1e5 + const iterations = Math.ceil(count/maxPoints) // 100k maximum per object + const objectList = [] + for (let i = 0; i < iterations; i++){ + const offset = i * maxPoints; + const thisCount = Math.min(count-offset, maxPoints) + const {material, updateValues} = createPointMaterial(thisCount, bivariate) + const biMul = bivariate ? 2 : 1 + const arrayOffset = offset*biMul + updateValues(textureData.subarray(arrayOffset, arrayOffset+(thisCount*biMul)) as Uint8Array); + objectList.push({ + material, + offset, + count:thisCount + }) } - const indexAttr = new THREE.Int32BufferAttribute(indexData, 1); - const maxPointsPerDraw = 2e31 - 1; // 32bit limit - const list = []; - for (let offset = 0; offset < subNumPoints; offset += maxPointsPerDraw) { - const count = Math.min(maxPointsPerDraw, subNumPoints - offset); - const geom = new THREE.BufferGeometry(); - geom.setAttribute('vertexIdx', indexAttr); - geom.setDrawRange(offset, count); - list.push(geom); - } - return list; - }, [depth, width, height]); - const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=> (new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - map: { value: volTexture }, - remapTexture: { value: remapTexture?? remapBorders }, - pointSize: {value: pointSize}, - scalePoints:{value: scalePoints}, - scaleIntensity: {value: scaleIntensity}, - timeScale: {value: depthRatio}, - aspect: {value: shape.x/shape.y}, - shape: {value: new THREE.Vector3(depth, height, width)}, - flatBounds:{value: new THREE.Vector4(xRange[0], xRange[1], zRange[0], zRange[1])}, - vertBounds:{value: new THREE.Vector2(yRange[0], yRange[1])}, - ...uniforms - }, - defines: { - GLOBAL_SCALE: globalscale*2, - ...(remapTexture ? { REPROJECT: true } : {}), - ...(disablePointScale ? { NO_SCALE: true } : {}) - }, - vertexShader: functionInjector(pointVert, colorScale), - fragmentShader:pointFrag, - depthWrite: true, - depthTest: true, - blending:THREE.NoBlending, - }) - ),[disablePointScale, remapTexture, colorScale]); - updateCommonUniforms(shaderMaterial); - useEffect(() => { - if (shaderMaterial) { - const uniforms = shaderMaterial.uniforms; - uniforms.map.value = volTexture; - shaderMaterial.needsUpdate = true; - uniforms.shape.value.set(depth, height, width); - uniforms.pointSize.value = pointSize; - uniforms.valueRange.value.set(valueRange[0], valueRange[1]); - uniforms.scalePoints.value = scalePoints; - uniforms.scaleIntensity.value = scaleIntensity; - uniforms.timeScale.value = depthRatio; - uniforms.flatBounds.value.set( - xRange[0], xRange[1], - zRange[0], zRange[1] - ); - flipY - ? uniforms.vertBounds.value.set(-yRange[1] , -yRange[0] ) - : uniforms.vertBounds.value.set(yRange[0] , yRange[1] ); - uniforms.aspect.value = shape.x/shape.y; - } - }, [volTexture, depthRatio, depth, height, shape, width, pointSize, valueRange, scalePoints, scaleIntensity, xRange, yRange, zRange]); - + u.pointShape.value = new THREE.Vector3(...dataShape) + return objectList + },[count, colorScale, bivariate]) + // --- UNIFORMS ---// + uniformUpdater(); + // Unique scaling for points + useEffect(()=>{ + u.volumeScale.value = shape; + u.flatBounds.value = new THREE.Vector4(-xRange[1],-xRange[0],zRange[0], zRange[1]); + u.vertBounds.value = new THREE.Vector2(yRange[0],yRange[1]); + u.dataShape.value = remapTexture + ? new THREE.Vector3(remapTexture.image.width, remapTexture.image.height, dataShape[0]) + : new THREE.Vector3(dataShape[2], dataShape[1], dataShape[0]) + },[shape, xRange, yRange, zRange, dataShape, flipY, remapTexture]) + + // --- CLEANUP --- // + useEffect(()=>()=>{ materialObjects.forEach(e=> e.material.dispose()) },[materialObjects]) return ( - + - {geometries.map((geom, idx) => ( - + {materialObjects.map((e, idx)=>( + ))} + diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index ea283b8ce..3eaa8e62c 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -1,18 +1,17 @@ -import React, {useRef, useMemo, useState, useEffect} from 'react' -import * as THREE from 'three' +import { evaluateColorMap } from '@/components/textures'; +import { uniformUpdater } from '@/components/textures/TSL/utils/uniformUpdater'; import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; -import { useShallow } from 'zustand/shallow' -import { parseUVCoords, GetTimeSeries, GetCurrentArray } from '@/utils/HelperFuncs'; -import { evaluateColorMap } from '@/components/textures'; -import { useCoordBounds } from '@/hooks/useCoordBounds' +import { useDimAxis } from '@/hooks'; +import { useCoordBounds } from '@/hooks/useCoordBounds'; +import { GetCurrentArray, GetTimeSeries, parseUVCoords } from '@/utils/HelperFuncs'; +import { useMemo, useEffect } from 'react'; +import * as THREE from 'three'; +import { useShallow } from 'zustand/shallow'; +import { createSphereMaterial } from '../textures/TSL/sphere'; import { SquareMeshes } from './TransectMeshes'; -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { useDimAxis, useValueScales } from '@/hooks'; -import { sphereVertex, sphereFrag } from '@/components/textures/shaders' -import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; -import { functionInjector } from '../ui/Elements/ColorAdjuster'; + function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number[]){ const lon = -Math.atan2(xyz.z,xyz.x) const lat = Math.asin(xyz.y); @@ -21,78 +20,33 @@ function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number return new THREE.Vector2(u,v) } -export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | undefined}) => { - const textures = usePaddedTextures(propTextures); - const {isFlat, dimNames, dimUnits, dataShape, strides, flipY, remapTexture, +export const Sphere = () => { + const { dimNames, dimUnits, dataShape, strides, flipY, setPlotDim,updateDimCoords, updateTimeSeries} = useGlobalStore(useShallow(s => ({ - isFlat: s.isFlat, dimNames: s.dimNames, dimUnits: s.dimUnits, - dataShape: s.dataShape, strides: s.strides, flipY: s.flipY, remapTexture: s.remapTexture, + dimNames: s.dimNames, dimUnits: s.dimUnits, + dataShape: s.dataShape, strides: s.strides, flipY: s.flipY, setPlotDim: s.setPlotDim, updateDimCoords: s.updateDimCoords, updateTimeSeries: s.updateTimeSeries }))) - const { selectTS, displacement, sphereResolution, fillValue, colorScale, + const { selectTS, sphereResolution, interpPixels, colorScale, getColorIdx, incrementColorIdx} = usePlotStore(useShallow(s => ({ - selectTS: s.selectTS, displacement: s.displacement, sphereResolution: s.sphereResolution, fillValue: s.fillValue, - colorScale: s.colorScale, getColorIdx: s.getColorIdx, incrementColorIdx: s.incrementColorIdx + selectTS: s.selectTS, sphereResolution: s.sphereResolution, interpPixels: s.interpPixels, colorScale: s.colorScale, + getColorIdx: s.getColorIdx, incrementColorIdx: s.incrementColorIdx }))) const {analysisMode, analysisArray} = useAnalysisStore(useShallow(s => s)) - const valueScales = useValueScales(); const {xArray, yArray, zArray} = useDimAxis(); const dimSlices = [zArray, yArray, xArray]; const geometry = useMemo(() => new THREE.IcosahedronGeometry(1, sphereResolution), [sphereResolution]); - const uniforms = useCommonUniforms() - - const shaderMaterial = useMemo(()=>{ - const shader = new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - map: { value: textures }, - remapTexture: { value: remapTexture }, - displaceZero: {value: -valueScales.minVal/(valueScales.maxVal-valueScales.minVal)}, - displacement: {value: displacement}, - ...uniforms - }, - defines:{ - ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: functionInjector(sphereVertex, colorScale), - fragmentShader: functionInjector(sphereFrag, colorScale), - blending: THREE.NormalBlending, - side:THREE.FrontSide, - transparent: true, - depthWrite:true, - }) - return shader - },[isFlat, colorScale, remapTexture]) + const shaderMaterial = useMemo( () => createSphereMaterial() , [interpPixels, colorScale]) // No reprojection on Sphere. Remains static and can't update - + uniformUpdater(); const backMaterial = useMemo(()=>{ const mat = shaderMaterial.clone() mat.side = THREE.BackSide; return mat; },[shaderMaterial]) - - const updateMaterial = (material: THREE.ShaderMaterial) => { - const uniforms = material.uniforms; - uniforms.map.value = textures; - uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) - uniforms.displacement.value = displacement - } - updateCommonUniforms(shaderMaterial); - updateCommonUniforms(backMaterial) - useEffect(()=>{ - if (shaderMaterial){ - updateMaterial(shaderMaterial) - } - if (backMaterial){ - updateMaterial(backMaterial) - } - },[textures, displacement, fillValue, valueScales]) - const {lonBounds, latBounds} = useCoordBounds() function HandleTimeSeries(event: THREE.Intersection){ const point = event.point.normalize(); - //const uv = XYZtoUV(point, texture?.source.data.width, texture?.source.data.height); const uv = XYZtoRemap(point, latBounds, lonBounds); uv.y = flipY ? 1 - uv.y : uv.y; @@ -130,7 +84,10 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture } updateDimCoords({[tsID] : dimObj}) } - + // --- CLEANUP --- // + useEffect(()=>()=>{ shaderMaterial.dispose() },[shaderMaterial]) + useEffect(()=>()=>{ geometry.dispose() },[geometry]) + useEffect(()=>()=>{ backMaterial.dispose() },[backMaterial]) return ( diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx index dc771d13a..b7133fd5b 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -1,25 +1,17 @@ -import React, { useEffect, useMemo } from 'react' -import { useGlobalStore } from '@/GlobalStates/GlobalStore'; -import { usePlotStore } from '@/GlobalStates/PlotStore'; +import * as u from '@/components/textures/TSL/utils/uniforms'; import { useErrorStore } from '@/GlobalStates/ErrorStore'; -import { useShallow } from 'zustand/shallow' -import * as THREE from 'three' -import { sphereBlocksFrag, sphereBlocksVert } from '../textures/shaders' -import { invalidate } from '@react-three/fiber' -import { usePaddedTextures } from '@/hooks/usePaddedTextures'; -import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; -import { functionInjector } from '../ui/Elements/ColorAdjuster'; -import { useCoordBounds, useDimAxis, useValueScales } from '@/hooks'; -const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | undefined}) => { - const textures = usePaddedTextures(propTextures); - const {isFlat, remapTexture} = useGlobalStore(useShallow(s => ({ - isFlat: s.isFlat, remapTexture: s.remapTexture - }))) - const { nanColor, nanTransparency, displacement, offsetNegatives, colorScale} = usePlotStore(useShallow(s => ({ - nanColor: s.nanColor, nanTransparency: s.nanTransparency, displacement: s.displacement, - offsetNegatives: s.offsetNegatives, colorScale: s.colorScale +import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { useDimAxis } from '@/hooks'; +import { uniformUpdater } from '@/components/textures/TSL/utils/uniformUpdater'; +import { invalidate, useThree } from '@react-three/fiber'; +import { useEffect, useMemo } from 'react'; +import * as THREE from 'three/webgpu'; +import { useShallow } from 'zustand/shallow'; +import { createSphereBlocksMaterial } from '../textures/TSL/sphereBlocks'; +const SphereBlocks = () => { + const { nanColor, nanTransparency, colorScale} = usePlotStore(useShallow(s => ({ + nanColor: s.nanColor, nanTransparency: s.nanTransparency, colorScale: s.colorScale }))) - const valueScales = useValueScales(); const {xArray, yArray} = useDimAxis() const width = xArray.length; const height = yArray.length; @@ -29,92 +21,50 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ useErrorStore.setState({ error:'largeArray' }) return 0 } + u.displacementResolution.value = new THREE.Vector2(width,height); return count },[width, height]) const geometry = useMemo(()=>{ const sqWidth = Math.PI*2; - const geo = new THREE.BoxGeometry(sqWidth/width, .05, sqWidth/height/2); - const uvs = new Float32Array(count * 2); - let idx = 0; - for (let i = 0; i < width; i++) { - for (let j = 0; j < height; j++) { - const u = (i + 0.5) / width; - const v = (j + 0.5) / height; - uvs[idx * 2] = u; - uvs[idx * 2 + 1] = v; - idx ++; - } - } - geo.setAttribute( - 'instanceUV', - new THREE.InstancedBufferAttribute(uvs, 2) - ); + const boxHeight = 0.05; + const geo = new THREE.BoxGeometry(sqWidth/width, boxHeight, sqWidth/height/2); + geo.translate(0, boxHeight / 2, 0); return geo - },[count]) - - const uniforms = useCommonUniforms() - const {lonBounds, latBounds} = useCoordBounds() - const shaderMaterial = useMemo(()=>{ - const shader = new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - map: { value: textures }, - remapTexture: { value: remapTexture }, - displaceZero: {value: offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal))}, - displacement: {value: displacement}, - widthFactor: {value: Math.abs(lonBounds[1]-lonBounds[0])/(2.0*Math.PI)}, - vertFactor: {value: Math.abs(latBounds[1]-latBounds[0])/(Math.PI)}, - ...uniforms - }, - defines:{ - ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: functionInjector(sphereBlocksVert, colorScale), - fragmentShader: sphereBlocksFrag, - blending:THREE.NoBlending, - depthWrite:true, - depthTest:true, - side: THREE.BackSide, - }) - return shader - },[isFlat, colorScale, remapTexture]) - updateCommonUniforms(shaderMaterial); - useEffect(()=>{ - if (shaderMaterial){ - const uniforms = shaderMaterial.uniforms; - uniforms.map.value = textures; - uniforms.displacement.value = displacement - uniforms.displaceZero.value = offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) - uniforms.widthFactor.value = Math.abs(lonBounds[1]-lonBounds[0])/(2.0*Math.PI) - uniforms.vertFactor.value = Math.abs(latBounds[1]-latBounds[0])/(Math.PI) - } - invalidate(); - },[valueScales, displacement, offsetNegatives, lonBounds, textures]) - - const nanMaterial = useMemo(()=>new THREE.MeshBasicMaterial({color:nanColor, opacity:(1-nanTransparency)}),[]) - nanMaterial.transparent = true; - + },[width, height]) + const material = useMemo(()=>createSphereBlocksMaterial(), [colorScale]) + const mesh = useMemo(()=>{ + const newMesh = new THREE.Mesh(geometry, material) + newMesh.count = count; + newMesh.frustumCulled = false; + return newMesh + },[geometry, material]) + const nanMaterial = useMemo(()=>{ + const material = new THREE.MeshBasicMaterial({color:nanColor, opacity:(1-nanTransparency)}) + material.transparent = true; + return material; + },[]) const nanSphereGeometry = useMemo(()=> new THREE.IcosahedronGeometry(1, 9),[]) - + // --- UNIFORMS ---// useEffect(()=>{ if (nanMaterial ){ - nanMaterial.dispose(); nanMaterial.color.set(nanColor); nanMaterial.opacity = (1-nanTransparency); invalidate(); } },[nanColor, nanTransparency]) - + uniformUpdater(); + // --- CLEANUP --- // + useEffect(()=>()=>{ material.dispose() },[material]) + useEffect(()=>()=>{ nanMaterial.dispose() },[nanMaterial]) + useEffect(()=>()=>{ geometry.dispose() },[geometry]) + useEffect(()=>()=>{ nanSphereGeometry.dispose() },[nanSphereGeometry]) return ( - + ) } -export {SphereBlocks} +export { SphereBlocks }; + diff --git a/src/components/plots/utils/CanvasText.tsx b/src/components/plots/utils/CanvasText.tsx new file mode 100644 index 000000000..15a5128e1 --- /dev/null +++ b/src/components/plots/utils/CanvasText.tsx @@ -0,0 +1,70 @@ +import { useMemo, useEffect, Children, JSX } from 'react'; +import * as THREE from 'three/webgpu'; + +type Props = { + children: React.ReactNode; + fontSize?: number; // world units (same meaning as drei's fontSize) + color?: string; + anchorX?: 'left' | 'center' | 'right'; + anchorY?: 'top' | 'middle' | 'bottom'; + font?: string; // CSS font-family + resolution?: number; // canvas px per em; raise for crisper text +} & Omit; + +const UNIT_PLANE = new THREE.PlaneGeometry(1, 1); + +export function CanvasText({ + children, fontSize = 0.1, color = '#fff', + anchorX = 'center', anchorY = 'middle', + font = 'sans-serif', resolution = 128, + ...meshProps +}: Props) { + const content = children as string ?? Children.toArray(children).join('') as string; + const { texture, width, height } = useMemo(() => { + const pad = resolution * 0.1; + const ctx = document.createElement('canvas').getContext('2d')!; + ctx.font = `${resolution}px ${font}`; + const textW = ctx.measureText(content).width; + const canvas = ctx.canvas; + canvas.width = Math.ceil(textW + pad * 2); + canvas.height = Math.ceil(resolution * 1.4); + + // resizing resets context state, so set it again + ctx.font = `${resolution}px ${font}`; + ctx.fillStyle = color; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(content, canvas.width / 2, canvas.height / 2); + + const tex = new THREE.CanvasTexture(canvas); + tex.colorSpace = THREE.SRGBColorSpace; + tex.anisotropy = 8; + tex.generateMipmaps = true; + tex.minFilter = THREE.LinearMipmapLinearFilter; + + const s = fontSize / resolution; // px -> world units + return { texture: tex, width: canvas.width * s, height: canvas.height * s }; + }, [children, color, fontSize, font, resolution]); + + const offsetX = anchorX === 'left' ? width / 2 : anchorX === 'right' ? -width / 2 : 0; + const offsetY = anchorY === 'top' ? -height / 2 : anchorY === 'bottom' ? height / 2 : 0; + + useEffect(() => () => texture.dispose(), [texture]); + + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/src/components/textures/ProjectionTexture.ts b/src/components/textures/ProjectionTexture.ts index 74ee263fe..b5eba0530 100644 --- a/src/components/textures/ProjectionTexture.ts +++ b/src/components/textures/ProjectionTexture.ts @@ -7,6 +7,7 @@ import proj4 from 'proj4'; import { getAxisIndices } from '@/hooks/useAxisIndices'; import { useZarrStore } from '@/GlobalStates/ZarrStore'; import { getDimAxis, getAxisDimAxis} from '@/hooks'; +import * as u from '@/components/textures/TSL/utils/uniforms' export function checkProjString(projString: string){ const {setError} = useErrorStore.getState() @@ -145,14 +146,14 @@ function createIrregularUV( texture.wrapT = THREE.ClampToEdgeWrapping; texture.flipY = flipY; texture.needsUpdate = true; - useGlobalStore.setState({remapBorders: texture}) + useGlobalStore.setState({remapBorders:texture}) } function createInverseUV( xArray: Array, yArray: Array, is360: boolean, - resolution : number + resolution : number, ) { // Creates an inverse UV map: for each normalized (x, y) position, // stores the (i, j) index into xArray/yArray that best matches it. @@ -178,7 +179,6 @@ function createInverseUV( const iIdx = findNearestIndex(normX, u); const iNorm = xArray.length > 1 ? iIdx / (xArray.length) : 0; const uValid = u >= xMin && u <= xMax; - const valid = uValid && vValid; data[ptr++] = THREE.DataUtils.toHalfFloat(iNorm); data[ptr++] = THREE.DataUtils.toHalfFloat(jNorm); @@ -186,7 +186,6 @@ function createInverseUV( ptr++; } } - const texture = new THREE.DataTexture( data, width, @@ -198,7 +197,6 @@ function createInverseUV( texture.minFilter = THREE.LinearFilter; texture.wrapS = THREE.ClampToEdgeWrapping; texture.wrapT = THREE.ClampToEdgeWrapping; - // texture.flipY = flipY; texture.needsUpdate = true; return texture; } @@ -209,7 +207,7 @@ export function handleIrregularGrid(){ const {flipY, borderCompatible} = useGlobalStore.getState() if (!borderCompatible) return; const isRegular = isUniformStep(xArray) && isUniformStep(yArray) - if (isRegular) return; + if (isRegular) return; const {is360Deg, plotType} = usePlotStore.getState(); if(plotType == 'sphere') { const texture = createInverseUV(xArray, yArray, is360Deg, 1024); @@ -221,7 +219,7 @@ export function handleIrregularGrid(){ export function reproject(resolution: number = 256){ - const {nativeCRS, destCRS, plotType, is360Deg} = usePlotStore.getState() + const {nativeCRS, destCRS, plotType, is360Deg, interpPixels} = usePlotStore.getState() const {remapTexture, flipY } = useGlobalStore.getState() let {xArray, yArray} = getDimAxis(); const insufficientCRS = !nativeCRS || !destCRS @@ -234,7 +232,7 @@ export function reproject(resolution: number = 256){ handleIrregularGrid() return; } - if (!checkProjString(destCRS) || !checkProjString(nativeCRS)) return; + if (!checkProjString(destCRS) || !checkProjString(nativeCRS)) return; const {xIdx, yIdx} = getAxisIndices() if (is360Deg) { xArray = remap360to180Monotonic(xArray) @@ -319,8 +317,11 @@ export function reproject(resolution: number = 256){ yTicks = flipY ? linspace(maxY, minY, targetHeight) : linspace(minY, maxY, targetHeight) - data = new Uint16Array(targetWidth * targetHeight * 4); + const fallBackU = (xArray.length - 0.5) / xArray.length + const fallBackV = (yArray.length - 0.5) / yArray.length + const halfWidth = xArray.length/2; + const halfHeight = yArray.length/2; for (let j = 0; j < targetHeight; j++) { for (let i = 0; i < targetWidth; i++) { const [x, y, valid] = safeInverse(proj, [xTicks[i], yTicks[j]]); @@ -330,10 +331,13 @@ export function reproject(resolution: number = 256){ const inBounds = valid === 1 && xi !== null &&yi !== null && Number.isFinite(xi) && Number.isFinite(yi); - let u = 0, v = 0; - if (inBounds) { + let u, v; + if (inBounds){ u = (xi + 0.5) / xArray.length; v = (yi + 0.5) / yArray.length; + } else{ // if invalid, we assume its near the edge. So we explicitly set the value to the edge of the UV + u = j > halfWidth ? fallBackU : 0; + v = i > halfHeight ? fallBackV : 0; } const idx = (j * targetWidth + i) * 4; data[idx] = THREE.DataUtils.toHalfFloat(u); @@ -387,7 +391,6 @@ export function reproject(resolution: number = 256){ }) } - function remap360to180Monotonic(arr: number[]) { const wrapped = arr.map(v => ((v + 180) % 360 + 360) % 360 - 180); const sorted = wrapped.sort((a, b) => a - b); diff --git a/src/components/textures/TSL/borders.ts b/src/components/textures/TSL/borders.ts new file mode 100644 index 000000000..fe6978207 --- /dev/null +++ b/src/components/textures/TSL/borders.ts @@ -0,0 +1,20 @@ +import { Discard, Fn, If, positionGeometry, vec4 } from 'three/tsl' +import * as THREE from 'three/webgpu' +import * as u from './utils/uniforms' +const fragShader = Fn(() =>{ + If( positionGeometry.x.lessThan( u.flatBounds.x ) + .or( positionGeometry.x.greaterThan( u.flatBounds.y ) ) + .or( positionGeometry.y.lessThan( u.vertBounds.x ) ) + .or( positionGeometry.y.greaterThan( u.vertBounds.y ) ) + .and( u.trim ), () => { + Discard(); + }); + return vec4(u.borderColor, 1); +}) + +export function createBorderMaterial(){ + const material = new THREE.NodeMaterial() + material.fragmentNode = fragShader(); + + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/dda.ts b/src/components/textures/TSL/dda.ts new file mode 100644 index 000000000..be06123d7 --- /dev/null +++ b/src/components/textures/TSL/dda.ts @@ -0,0 +1,110 @@ +import { + abs, any, bool, Break, cameraPosition, cameraWorldMatrix, clamp, + Discard, EPSILON, float, floor, Fn, fract, greaterThanEqual, If, int, + ivec3, lessThan, Loop, max, modelWorldMatrixInverse, normalize, + positionLocal, select, sign, texture, varying, vec2, vec3, vec4 +} from 'three/tsl'; +import * as THREE from 'three/webgpu'; +import * as h from './utils/commonHelpers'; +import * as u from './utils/uniforms'; +import { colorTexel, hitBox, sampleVoxel, shouldSkip } from './utils/volumeHelpers'; + +const ddaColor = Fn(([vOrigin, vDirection] : [any, any]) =>{ + const rayDir = normalize( vDirection ).toVar("rayDir"); + const bounds = hitBox( vOrigin, rayDir ).toVar("bounds"); + If(bounds.x.greaterThan(bounds.y), () => { Discard(); }); + bounds.x.assign(max(bounds.x, 0.0)); + + const gridRes = u.dataShape; + const boxMin = u.volumeScale.mul( 0.5 ).negate(); + const voxelSize = u.volumeScale.div( gridRes ); + + // Initial voxel from the ray's entry point into the box. + const p0 = vOrigin.add( bounds.x.mul( rayDir ) ); + const localPos = clamp( p0.sub( boxMin ).div( u.volumeScale ), vec3( 0.0 ), vec3( 1.0 ).sub( vec3( EPSILON ) ) ).mul( gridRes ); + const voxel = clamp( ivec3( floor( localPos ) ), ivec3( 0 ), ivec3( gridRes ).sub( 1 ) ).toVar("voxel"); + // Standard 3D DDA (Amanatides & Woo) setup. + const stepDir = sign( rayDir ); + const stepDirI = ivec3( stepDir ); + const nextBoundary = boxMin.add( vec3( voxel ).add( max( stepDir, 0.0 ) ).mul( voxelSize ) ); + const tMax = nextBoundary.sub( vOrigin ).div( rayDir ).toVar("tMax"); + const tDelta = voxelSize.div( abs( rayDir ) ).toVar("tDelta"); + // // Handle parallel rays. If ray has < eps component in an axis, it will never cross that axis + // // If it is degen just manually set the interesction distance to someting arbitrarly high + const degenerate = lessThan( abs( rayDir ), vec3( EPSILON ) ); + tMax.assign( select(degenerate, vec3( 1e30 ), tMax )); + tDelta.assign( select( degenerate, vec3( 1e30 ), tDelta )); + const t = bounds.x.toVar('t'); + const maxSteps = int( gridRes.x.add( gridRes.y ).add( gridRes.z ) ).mul( 2 ).add( 8 ); + const accumColor = vec3( 0.0 ).toVar('accumColor'); + const alphaAcc = float( 0.0 ).toVar('alphaAcc'); + const borderHit = bool(false).toVar("borderHit"); + + // //Steps + Loop( maxSteps, () => { + If( t.greaterThan( bounds.y ), () => {Break();}); + // Sample at the center of the current voxel. + const pCenter = boxMin.add( vec3( voxel ).add( 0.5 ).mul( voxelSize ) ); + const texCoord = vec3(0).toVar("texCoord"); + const maskUV = vec2(0).toVar("maskUV"); + const skip = shouldSkip(pCenter, texCoord, maskUV) as any; + + If(skip.not(), () => { + const d = float(0.0).toVar(); + const biVal = float(0.0).toVar(); + const isNan = bool(false).toVar(); + const inRange = sampleVoxel(texCoord, d, biVal, isNan); + If(inRange, () => { + colorTexel(d, biVal, isNan, accumColor, alphaAcc) + If(alphaAcc.greaterThanEqual(0.99), () => { + If(u.useBorderTexture, () => { + const pHit = vOrigin.add(t.mul(rayDir)).toVar(); + const localPosContinuous = pHit.xy.sub(boxMin).div(u.volumeScale).toVar(); + const borderUV = h.reprojectMask(localPosContinuous.xy, bool(true)) + borderHit.assign(h.isBorder(borderUV)) + }); + Break(); + }); + }); + }); + If(tMax.x.lessThan(tMax.y).and(tMax.x.lessThan(tMax.z)), () => { + t.assign(tMax.x); + tMax.x.addAssign(tDelta.x); + voxel.x.addAssign(stepDirI.x); + }).ElseIf(tMax.y.lessThan(tMax.z), () => { + t.assign(tMax.y); + tMax.y.addAssign(tDelta.y); + voxel.y.addAssign(stepDirI.y); + }).Else(() => { + t.assign(tMax.z); + tMax.z.addAssign(tDelta.z); + voxel.z.addAssign(stepDirI.z); + }); + + If(any(lessThan( voxel, ivec3( 0 ) )) + .or(any( greaterThanEqual( voxel, ivec3( gridRes )))), () =>{ + Break(); + }); + }) + If( alphaAcc.lessThanEqual( 0.0 ), () => Discard()); + return select(borderHit, vec4(u.borderColor, 1.0), vec4(accumColor, alphaAcc)); +}) + +export const createDDAMaterial = (isOrtho=false) => { + const material = new THREE.NodeMaterial(); + material.transparent = true; + let vOrigin, vDirection; + if (isOrtho){ + vOrigin = varying(positionLocal, 'vOrigin'); + const viewDirWorld = cameraWorldMatrix.mul(vec4(0.0, 0.0, -1.0, 0.0)); + const viewDirLocal = modelWorldMatrixInverse.mul(viewDirWorld).xyz; + vDirection = varying(normalize(viewDirLocal), 'vDirection'); + } else { + vOrigin = varying(modelWorldMatrixInverse.mul(vec4(cameraPosition, 1)).xyz); + vDirection = varying(positionLocal.sub(vOrigin)); + material.side = THREE.BackSide; + } + material.colorNode = ddaColor(vOrigin, vDirection); + + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/flatBlocks.ts b/src/components/textures/TSL/flatBlocks.ts new file mode 100644 index 000000000..b589dd277 --- /dev/null +++ b/src/components/textures/TSL/flatBlocks.ts @@ -0,0 +1,101 @@ +import { + bool,clamp,float,Fn, If,instancedArray, + instanceIndex,mix,positionGeometry,positionLocal, + positionWorld, + select,struct,varying,vec2,vec3,vec4 +} from 'three/tsl'; +import * as THREE from 'three/webgpu'; +import * as h from './utils/commonHelpers'; +import * as u from './utils/uniforms'; + +const instanceColor = Fn(([strengths] : [any])=>{ + if (u.bivariate.value){ + const biCol = h.colorMixer( strengths.x, strengths.y ).toVar(); + return vec4(biCol.rgb, 1) + } else{ + const color = u.cmap.sample(vec2(strengths.x, 0.5)) + return vec4(color.rgb, 1) + } +}) +const isBorder = Fn(([instanceUV] : [any])=>{ + const isBorder = bool(false).toVar(); + If(u.useBorderTexture, ()=>{ + const resolution = u.displacementResolution + const aspect = resolution.x.toFloat().div(resolution.y) + const gridExtent = vec2(4, float(4).div(aspect)).toVar(); + const thisUV = instanceUV.add(positionGeometry.xy.div(gridExtent)); + isBorder.assign(h.isBorder(thisUV.fract())); + }) + return isBorder; +}) +const getUVs = Fn(([thisUV, isValid] : [any, any]) =>{ + const instanceUV = thisUV.toVar(); + const maskUV = instanceUV.toVar(); + const valid = h.reprojector(instanceUV, maskUV).toVar(); + isValid.assign(valid) + return vec4(instanceUV, maskUV) +}) + +export function createFlatBlocksMaterial(count: number){ + + // --- VERTEX --- // + const resolution = u.displacementResolution + const width = resolution.x; + const height = resolution.y; + const thisUV = Fn(() => { + const resolution = u.displacementResolution + const px = instanceIndex.mod(width); + const py = instanceIndex.div(width).mod(height); + return vec2( + px.toFloat().add(0.5), + py.toFloat().add(0.5) + ).div(resolution) + })() + const position = Fn(() => { + const aspect = height.toFloat().div(width) + const posX = thisUV.x.sub(0.5).mul(2); + const posY = thisUV.y.sub(0.5).mul(2).mul(aspect) + return vec2(posX, posY) + })() + + const material = new THREE.NodeMaterial(); + const isValid = bool(true).toVar(); + const uvInfo = varying(getUVs(thisUV, isValid)) + const newV = uvInfo.xy.toVar(); + const maskUV = uvInfo.zw.toVar(); + const {localCoord, textureIdx} = h.getLocalCoord(newV) + let strength, biVal, isNan; + let colorIn; // what the fragment stage receives: vec2(first, second), unswapped for bivariate + if (u.bivariate.value){ + const raw = h.sample2(localCoord, textureIdx); + isNan = h.isNaNBits(raw.r).or(h.isNaNBits(raw.g)) + .or(u.useF16.not().and(raw.r.equal(1.0))) + .or(u.useF16.not().and(raw.g.equal(1.0))); + strength = mix(raw.r, raw.g, u.bivariateSelection) // For some reason select and Ifs don't work as expected on vertex/varying. Mix works though + biVal = mix(raw.g, raw.r, u.bivariateSelection) + colorIn = varying(raw, 'vBivar'); + } else { + strength = h.sample1(localCoord, textureIdx).toVar(); + strength = h.rescaler(strength); + isNan = h.isNaNBits(strength).or(u.useF16.not().and(strength.equal(1.0))); + strength = clamp(strength.mul(u.cScale).add(u.cOffset), 0.0, 0.995); + biVal = float(0); + colorIn = vec2(varying(strength, 'vStrength'), float(0)); + } + const heightFactor = strength.sub(u.displaceZero).mul(u.displacement); + const scaledPosition = vec3( + positionLocal.x, positionLocal.y, positionLocal.z.mul(heightFactor) + ) + const newPos = scaledPosition.add(vec3(position, 0)); + const clipped = strength.greaterThan(u.threshold.y) + .or(strength.lessThan(u.threshold.x)) + .or(isValid.not()) + .or(isNan) + const masked = h.maskOut(maskUV) + material.positionNode = select(clipped.or(masked), vec3(0), newPos); + + // --- FRAG ---// + const borderHit = isBorder(maskUV) + material.colorNode = select(borderHit, u.borderColor, instanceColor(colorIn)); + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/flatMap.ts b/src/components/textures/TSL/flatMap.ts new file mode 100644 index 000000000..d905802f1 --- /dev/null +++ b/src/components/textures/TSL/flatMap.ts @@ -0,0 +1,88 @@ +import { abs, bool, Discard, float, Fn, If, min, mix, select, struct, uv, vec2, vec4 } from 'three/tsl'; +import * as THREE from 'three/webgpu'; +import * as h from './utils/commonHelpers'; +import * as u from './utils/uniforms'; + +const twoVs = struct({ + remap: 'vec2', + maskUV: 'vec2', + valid: 'bool' +}); +const getUVs = Fn(([thisUV] : [any]) =>{ + const newV = thisUV.toVar("instanceUV"); + const maskUV = newV.toVar('maskUV'); + const valid = h.reprojector(newV, maskUV); + return twoVs(newV, maskUV, valid) +}) + +const maskAndBorder = (thisUV: any) =>{ + const masked = h.maskOut(thisUV); + const isBorder = h.isBorder(thisUV); + const maskColor = select(masked, vec4(u.nanColor, u.nanAlpha), vec4(-1),); + const borderColor = select(isBorder, vec4(u.borderColor, 1), vec4(-1)); + + return maskColor.max(borderColor); +} + +const faceColor = Fn( ([thisUV] : [any]) => { + const Color = vec4( 0 ).toVar( 'Color' ); + const {localCoord, textureIdx} = h.getLocalCoord(thisUV) + const strength = float( 0 ).toVar( 'strength' ); + const biVal = float( 0 ).toVar( 'biVal' ); + const biNaN = bool( false ).toVar( 'biNaN' ); + + If( u.bivariate, () => { + const bivar = h.sample2ToOrder( localCoord, textureIdx, u.bivariateSelection ); + strength.assign( bivar.r ); + biVal.assign( bivar.g ); + biNaN.assign( h.isNaNBits( strength ).or( h.isNaNBits( biVal ) ) ); + } ).Else( () => { + strength.assign( h.sample1( localCoord, textureIdx ) ); + strength.assign(h.rescaler(strength)) + } ); + + const isNan = h.isNaNBits( strength ).or( biNaN ).or( u.useF16.not().and( strength.equal( 1. ) ) ); + + If( isNan, () => { + Color.assign( vec4( u.nanColor, u.nanAlpha ) ); + } ).Else( () => { + If( u.bivariate, () => { + const flipOrder = u.bivariateSelection.notEqual( 0 ); + Color.assign( vec4( select( flipOrder, h.colorMixer( biVal, strength ), h.colorMixer( strength, biVal ) ), 1. ) ); + } ).Else( () => { + strength.mulAssign( u.cScale ); + strength.assign( min( strength.add( u.cOffset ), 0.995 ) ); + Color.assign( vec4( u.cmap.sample( vec2( strength, 0.5 ) ).rgb, 1. ) ); + } ); + } ); + + const valid = strength.greaterThanEqual( u.threshold.x ).and( strength.lessThanEqual( u.threshold.y ) ); + If( valid.not().or( abs( strength.sub( u.fillValue ) ).lessThan( 0.005 ) ), () => { + Color.assign( vec4( 0. ) ); + } ); + return Color; +} ); + +export function createFlatMapMaterial(){ + const material = new THREE.NodeMaterial(); + material.transparent = true; + material.side = THREE.DoubleSide; + material.colorNode = Fn( () => { + const Color = vec4( 0 ).toVar( 'FinalColor' ); + const texCoord = uv().toVar( 'texCoord' ); + const uvInfo = getUVs(texCoord) + const maskUV = uvInfo.get('maskUV') + const isValid = uvInfo.get('valid') + const newV = uvInfo.get('remap') + If(isValid.not(), () =>{ Discard(); }) + const maskNBorders = maskAndBorder(maskUV).toVar( 'early' ); + If( maskNBorders.a.greaterThanEqual( 0 ), () => { + Color.assign( maskNBorders ); + } ).Else( () => { + Color.assign( faceColor(newV) ); + } ); + return Color; + } )(); + return material +} + diff --git a/src/components/textures/TSL/index.ts b/src/components/textures/TSL/index.ts new file mode 100644 index 000000000..08990afe2 --- /dev/null +++ b/src/components/textures/TSL/index.ts @@ -0,0 +1,2 @@ +export {createRayMarchingMaterial} from './rayMarcher' +export {createDDAMaterial} from './dda' \ No newline at end of file diff --git a/src/components/textures/TSL/morphingPoints.ts b/src/components/textures/TSL/morphingPoints.ts new file mode 100644 index 000000000..6983fc220 --- /dev/null +++ b/src/components/textures/TSL/morphingPoints.ts @@ -0,0 +1,97 @@ +import * as u from '@/components/textures/TSL/utils/uniforms'; +import { clamp, cos, float, instancedArray, instanceIndex, min, positionWorld, rotate, sin, smoothstep, texture, time, uniform, vec2, vec3, vec4 } from "three/tsl"; +import * as THREE from 'three/webgpu'; + + +function getShapePositions(count:number){ + const spherePositions = new Float32Array(count * 3); + const cubePositions = new Float32Array(count * 3); + const planePositions = new Float32Array(count * 3); + + // --- Sphere Positions (using Fibonacci lattice for even distribution) --- + const phi = Math.PI * (3.0 - Math.sqrt(5.0)); // Golden angle + for (let i = 0; i < count; i++) { + const y = 1 - (i / (count - 1)) * 2; // y goes from 1 to -1 + const radius = Math.sqrt(1 - y * y); + const theta = phi * i; + + const x = Math.cos(theta) * radius; + const z = Math.sin(theta) * radius; + + spherePositions[i * 3] = x * 1.2; + spherePositions[i * 3 + 1] = y * 1.2; + spherePositions[i * 3 + 2] = z * 1.2; + } + + // --- Cube Positions (16x16x16 grid) --- + const cubRes = 25 + let i = 0; + for (let x = 0; x < cubRes; x++) { + for (let y = 0; y < cubRes; y++) { + for (let z = 0; z < cubRes; z++) { + cubePositions[i * 3] = (x / cubRes - 0.5) * 2; + cubePositions[i * 3 + 1] = (y / cubRes - 0.5) * 2; + cubePositions[i * 3 + 2] = (z / cubRes - 0.5) * 2; + i++; + } + } + } + + // --- Plane Positions (64x64 grid) --- + const planeRes = 125 + i = 0; + for (let x = 0; x < planeRes; x++) { + for (let y = 0; y < planeRes; y++) { + planePositions[i * 3] = (x / planeRes - 0.5) * 2.5 ; + planePositions[i * 3 + 1] = (y / planeRes - 0.5) * 2.5 ; + planePositions[i * 3 + 2] = 0; + i++; + } + } + return {spherePositions, cubePositions, planePositions} +} + +export const pointerPos = uniform(vec2(-100)) + +export function createMorphingMaterial(points=15625){ + const {spherePositions, cubePositions, planePositions} = getShapePositions(points) + const sPositions = instancedArray(spherePositions, 'vec3'); + const cPositions = instancedArray(cubePositions, 'vec3'); + const pPositions = instancedArray(planePositions, 'vec3'); + + const material = new THREE.SpriteNodeMaterial(); + + const sPos = sPositions.element(instanceIndex); + const cPos = cPositions.element(instanceIndex); + const pPos = pPositions.element(instanceIndex); + const holdTime = 5; // seconds sitting on a shape + const moveTime = 2; // seconds morphing to the next one + const segment = holdTime + moveTime; + + // 0..3, advances by 1 per segment, wraps S -> C -> P -> S + const stage = time.div(segment).mod(3); + + // stays on integers while holding, eases to the next integer at the end of each segment + const hold = holdTime / segment; + const e = stage.floor().add(smoothstep(hold, 1, stage.fract())); // 0..3 + + // triangle weights, always sum to 1; e=3 wraps back to the sphere + const wS = clamp(float(1).sub(e), 0, 1).add(clamp(e.sub(2), 0, 1)); + const wC = clamp(float(1).sub(e.sub(1).abs()), 0, 1); + const wP = clamp(float(1).sub(e.sub(2).abs()), 0, 1); + + const morphedPosition = sPos.mul(wS).add(cPos.mul(wC)).add(pPos.mul(wP)).toVar(); + const rotateX = rotate(morphedPosition,vec3(1,0,0).mul(time.div(20))) + const rotateY = rotate(rotateX,vec3(0,1,0).mul(time.div(15))) + + const r = sin( positionWorld.z.add( time.mul( 0.2 ) ) ); + const g = cos( positionWorld.y.add( time.mul( 0.3 ) ) ); + const b = cos( positionWorld.x.add( positionWorld.y ).add( time.mul( 0.5 ) ) ); + const mag = min( sin( r.add( g ).add( b ) ).add( 1. ).div( 2. ), 0.996 ); + + // const distance = pointerPos.distance(positionWorld.mul(vec3(1,1,0))).inverseSqrt().clamp(1,100) + material.positionNode = rotateY + material.colorNode = vec4(texture(u.cmap, vec2(mag, 0.5)).rgb, 1) + material.scaleNode = float(0.015) + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/points.ts b/src/components/textures/TSL/points.ts new file mode 100644 index 000000000..666d74621 --- /dev/null +++ b/src/components/textures/TSL/points.ts @@ -0,0 +1,104 @@ +import { + bool,float,Fn,fract,If,instancedArray,instanceIndex,int,min,mod,texture,uint, + uniform,userData,varying,vec2,vec3,vec4 +} from 'three/tsl'; +import * as THREE from 'three/webgpu'; +import * as h from './utils/commonHelpers'; +import * as u from './utils/uniforms'; + +const GLOBAL_SCALE = 1/500 + +export function createPointMaterial(count: number, bivariate: boolean){ + const thisCount = bivariate ? count*2: count; + const values = instancedArray(Math.ceil(thisCount / 4), 'uint'); + + const depth = uint( u.pointShape.x ); + const height = uint( u.pointShape.y ); + const width = uint( u.pointShape.z ).toVar(); + const aspect = height.toFloat().div(width) + const timeAspect = depth.toFloat().div(width) + const offset = userData('offset', 'int') + const globalIndex = instanceIndex.add(offset) + const texCoord = Fn(() => { + const normX = globalIndex.mod(width).toFloat().div(width.sub(1)).toVar() + const normY = globalIndex.div(width).mod(height).toFloat().div(height.sub(1)).toVar() + const normZ = globalIndex.div(height.mul(width)).toFloat().div(depth.sub(1)).toVar() + return vec3(normX, normY, fract(normZ.sub(u.animateProg))); + })() + const positon = Fn(()=>{ + const px = texCoord.x.sub(0.5).mul(2) + const py = texCoord.y.sub(0.5).mul(2).mul(aspect) + const pz = texCoord.z.sub(0.5).mul(2).mul(timeAspect) + const scale = width.toFloat().mul(GLOBAL_SCALE) + return vec3(px, py, pz).mul(scale); + })() + + function updateValues(array: Uint8Array) { + const attr = values.value; + const u8 = new Uint8Array(attr.array.buffer, attr.array.byteOffset, attr.array.byteLength) + // u8.fill(0) + u8.set(array) + attr.needsUpdate = true + } + const getByte = Fn(([index]: [any]) => { + // This is an AI function. I don't understand it fully. + const i = uint(index); + const word = values.element(i.shiftRight(2)); // i / 4 + const shift = i.bitAnd(3).mul(8); // (i % 4) * 8 + return word.shiftRight(shift).bitAnd(0xFF); // uint 0..255 + }); + + const thisIndex = instanceIndex.mul(int(Number(bivariate) + 1)).toVar() + const sizeIndex = bivariate ? thisIndex.add(int(u.bivariateSelection)) : thisIndex // The index used for scaling the points + const rawValue = float(getByte(sizeIndex)).div(255).toVar(); + const isNaN = bool(false).toVar(); + const spriteValue = varying(h.rescaler(rawValue)) + const spriteCoord = texCoord + const pointColor = Fn(()=>{ + const color = vec3(0); + If(u.bivariate, ()=>{ + const v1 = float(getByte(thisIndex)).div(255) + const v2 = float(getByte(thisIndex.add(1))).div(255) + color.assign(h.colorMixer(v1, v2)) + const v1Nan = h.isNaNBits(v1).or(u.useF16.not().and(v1.equal(1.0))) + const v2Nan = h.isNaNBits(v2).or(u.useF16.not().and(v2.equal(1.0))) + isNaN.assign(v1Nan.or(v2Nan)) + }).Else(()=>{ + const remapped = spriteValue.mul( u.cScale ).toVar(); + remapped.assign( min( remapped.add( u.cOffset ), 0.995 ) ); + color.assign(u.cmap.sample(vec2(remapped,0.5)).rgb) + isNaN.assign(h.isNaNBits(rawValue).or(u.useF16.not().and(rawValue.equal(1.0)))) + + }) + return color; + })() + + const pointScaler = Fn(()=>{ + const pointScale = u.pointSize.toVar() + const p = spriteCoord + const multiplier = float(0.01); + pointScale.mulAssign(multiplier) + If(u.scaleByVal, ()=>{ + const strength = spriteValue.pow(u.scaleIntensity); + pointScale.mulAssign(strength) + }) + const outOfBounds = p.x.greaterThan(u.flatBounds.x.negate()) + .or(p.x.lessThan(u.flatBounds.y.negate())) + .or(p.z.negate().greaterThan(u.flatBounds.z.negate())) + .or(p.z.negate().lessThan(u.flatBounds.w.negate())) + .or(p.y.lessThan(u.vertBounds.x)) + .or(p.y.greaterThan(u.vertBounds.y)) + .or(spriteValue.greaterThan(u.threshold.y)) + .or(spriteValue.lessThan(u.threshold.x)) + .or(isNaN); + If(outOfBounds, ()=>{ + pointScale.assign(0) + }) + return vec2(pointScale) + }) + const material = new THREE.SpriteNodeMaterial() + material.positionNode = positon.mul(vec3(1,1,u.timeScale)) + material.scaleNode = pointScaler() + material.colorNode = vec4(pointColor, 1); + return {material, updateValues} +} \ No newline at end of file diff --git a/src/components/textures/TSL/rayMarcher.ts b/src/components/textures/TSL/rayMarcher.ts new file mode 100644 index 000000000..17500d468 --- /dev/null +++ b/src/components/textures/TSL/rayMarcher.ts @@ -0,0 +1,68 @@ +import { Fn, vec4, vec3, abs, If, normalize, pow, EPSILON, Discard, int, ceil, Loop, + min, max, vec2, select, cameraPosition, positionLocal, varying, float, bool, + Break, modelWorldMatrixInverse, cameraWorldMatrix, Continue, + texture, mod, clamp, ivec3, fract, + uv} from 'three/tsl'; +import * as u from './utils/uniforms' +import * as h from './utils/commonHelpers'; +import * as THREE from 'three/webgpu' +import { colorTexel, hitBox, sampleVoxel, shouldSkip } from './utils/volumeHelpers'; + +const rayMarch = Fn(([vOrigin, vDirection] : [any, any]) => { + const rayDir = normalize(vDirection); + const bounds = hitBox( vOrigin, rayDir ).toVar("bounds"); + If(bounds.x.greaterThan(bounds.y), () => { Discard(); }); + bounds.x.assign(max(bounds.x, 0.0)); + + const inc = vec3(1.0).div(abs(rayDir)); + const delta = min(inc.x, min(inc.y, inc.z)).div(u.steps).toVar('delta'); + const accumColor = vec3(0.0).toVar('accumRGB'); + const alphaAcc = float(0.0).toVar('aphaAcc'); + const borderHit = bool(false).toVar('borderHit'); + + const nSteps = int(ceil(bounds.y.sub(bounds.x).div(delta))); + Loop(nSteps, ({ i }: { i: any }) => { + const t = bounds.x.add(float(i).mul(delta)); + const p = vOrigin.add(rayDir.mul(t)).toVar("p"); + const texCoord = vec3(0).toVar('texCoord') + const maskUV = vec2(0).toVar("maskUV") + const skip = shouldSkip(p,texCoord, maskUV ); + If(skip.not(), () => { + const d = float(0.0).toVar(); + const biVal = float(0.0).toVar(); + const isNan = bool(false).toVar(); + const inRange = sampleVoxel(texCoord, d, biVal, isNan); + + If(inRange, () => { + colorTexel(d, biVal, isNan, accumColor, alphaAcc) + If(alphaAcc.greaterThanEqual(0.99), () => { + If(u.useBorderTexture, () => { + borderHit.assign(h.isBorder(maskUV)) + }); + Break(); + }); + }); + }); + }); + If( alphaAcc.lessThanEqual( 0.0 ), () => Discard()); + return select(borderHit, vec4(u.borderColor, 1.0), vec4(accumColor, alphaAcc)); +}) + +export function createRayMarchingMaterial(isOrtho=false){ + const material = new THREE.NodeMaterial(); + let vOrigin, vDirection; + if (isOrtho){ + vOrigin = varying(positionLocal, 'vOrigin'); + const viewDirWorld = cameraWorldMatrix.mul(vec4(0.0, 0.0, -1.0, 0.0)); + const viewDirLocal = modelWorldMatrixInverse.mul(viewDirWorld).xyz; + vDirection = varying(normalize(viewDirLocal), 'vDirection'); + } else { + vOrigin = varying(modelWorldMatrixInverse.mul(vec4(cameraPosition, 1)).xyz); + vDirection = varying(positionLocal.sub(vOrigin)); + material.side = THREE.BackSide; + } + material.fragmentNode = rayMarch(vOrigin, vDirection); + material.transparent = true; + material.depthWrite = false; + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/sphere.ts b/src/components/textures/TSL/sphere.ts new file mode 100644 index 000000000..acad286fd --- /dev/null +++ b/src/components/textures/TSL/sphere.ts @@ -0,0 +1,148 @@ +import * as u from './utils/uniforms' +import * as h from './utils/commonHelpers'; +import * as THREE from 'three/webgpu' +import { Fn, normalize, asin, atan, PI, mul, mod, vec2, vec4, If, vec3, float, bool, + fract, select, min, clamp, ivec3, all, cos, positionGeometry, greaterThanEqual, lessThanEqual, + int, + texture} from 'three/tsl'; + +const giveUV = Fn(( [position] : [any]) => { + const n = normalize( position ); + const latitude = asin( n.y ).toVar(); + const longitude = atan( n.z, n.x ).negate().toVar(); + latitude.assign( latitude.sub( u.latBounds.x ).div( u.latBounds.y.sub( u.latBounds.x ) ) ); + const span = u.lonBounds.y.sub( u.lonBounds.x ); + longitude.assign( mod( longitude.sub( u.lonBounds.x ), mul( 2.0, PI ) ) ); + longitude.assign( longitude.div( span ) ); + return vec2( longitude, latitude ); +}) + +const giveMaskUV = Fn( ( [ position ] : [any] ) => { + const n = normalize( position ); + const latitude = asin( n.y ); + const longitude = atan( n.z, n.x ).negate(); + latitude.divAssign( PI ); + longitude.divAssign( mul( 2., PI ) ); + const u = longitude.add( 0.5 ); + const v = latitude.add( 0.5 ); + return vec2( u, v ); +}); + +const maskAndBorder = Fn( () => { + const result = vec4( 0, 0, 0, - 1 ).toVar( 'maskResult' ); + If( u.maskValue.notEqual( 0 ).or( u.useBorderTexture ), () => { + const maskUV = giveMaskUV( positionGeometry ); + If( u.is360, () => { + maskUV.x.assign( fract( maskUV.x ) ); + }); + If( u.maskValue.notEqual( 0 ), () => { + //@ts-ignore level does exist on this node + const mask = u.maskTexture.sample( maskUV ).level(0).r; + const cond = select( u.maskValue.equal( 1 ), mask.lessThan( 0.5 ), mask.greaterThanEqual( 0.5 ) ); + If( cond, () => { + result.assign( vec4( u.nanColor, 1. ) ); + result.a.assign( u.nanAlpha ); + }); + }) + If(u.useBorderTexture, () => { + //@ts-ignore level does exist on this node + const borderDist = u.borderTexture.sample( maskUV ).level(0).r; + const latFac = cos( maskUV.y ); + If( borderDist.lessThanEqual( u.borderWidth.mul( latFac ) ), () => { + result.assign( vec4( u.borderColor, 1.0 ) ); + }); + }); + } ); + return result; +}); + +const sphereColor = Fn( () => { + const Color = vec4( 1,0,0,1 ).toVar( 'Color' ); + const aPosition = positionGeometry.toVar('aPosition'); + const sampleCoord = giveUV(aPosition).toVar('sampleCoord'); + const inBounds = all( greaterThanEqual( sampleCoord, vec2( 0.0 ) ) ) + .and( all( lessThanEqual( sampleCoord, vec2( 1.0 ) ) ) ); + If( inBounds, () => { + If(u.remapBorders, ()=>{ + sampleCoord.xy.assign(texture(u.remapTexture, h.realCoords(sampleCoord.xy))) + }) + const {localCoord, textureIdx} = h.getLocalCoord(sampleCoord) + // Scale up + localCoord.assign( fract( localCoord ) ); + const strength = float( 0 ).toVar( 'strength' ); + const biVal = float( 0 ).toVar( 'biVal' ); + const biNaN = bool( false ).toVar( 'biNaN' ); + If( u.bivariate, () => { + const bivar = h.sample2ToOrder( localCoord, textureIdx, u.bivariateSelection ); + strength.assign( bivar.r ); + biVal.assign( bivar.g ); + biNaN.assign( h.isNaNBits( strength ).or( h.isNaNBits( biVal ) ) ); + } ).Else( () => { + strength.assign( h.sample1( localCoord, textureIdx ) ); + } ); + const isNan = h.isNaNBits( strength ).or( biNaN ).or( u.useF16.not().and( strength.equal( 1. ) ) ); + If( isNan, () => { + Color.assign( vec4( u.nanColor, u.nanAlpha ) ); + } ).Else( () => { + If( u.bivariate, () => { + const flipOrder = u.bivariateSelection.notEqual( 0 ); + Color.assign( vec4( select( flipOrder, h.colorMixer( biVal, strength ), h.colorMixer( strength, biVal ) ), 1. ) ); + } ).Else( () => { + strength.assign(h.rescaler(strength)); + strength.mulAssign( u.cScale ); + strength.assign( min( strength.add( u.cOffset ), 0.995 ) ); + Color.assign( vec4( u.cmap.sample( vec2( strength, 0.5 ) ).rgb, 1. ) ); + } ); + } ); + const valid = strength.greaterThanEqual( u.threshold.x ).and( strength.lessThanEqual( u.threshold.y ) ); + If( valid.not(), () => { + Color.assign( vec4( 0. ) ); + }); + }).Else(() => {Color.assign( vec4( u.nanColor, u.nanAlpha ) )}); + return Color +}) + +const sphereVertex = Fn(() =>{ + const sampleCoord = giveUV( positionGeometry ).toVar("sampleCoord"); + const inBounds = all( greaterThanEqual( sampleCoord, vec2( 0.0 ) ) ).and( all( lessThanEqual( sampleCoord, vec2( 1.0 ) ) ) ); + const newPosition = positionGeometry.toVar('newPosition'); + If( inBounds, () => { + const normal = normalize( positionGeometry ); + const zStepSize = int( u.textureDepths.y ).mul( int( u.textureDepths.x ) ); + const yStepSize = int( u.textureDepths.x ); + const texCoord = vec3( sampleCoord.xy, u.animateProg ); + const idx = clamp( ivec3( texCoord.mul( u.textureDepths ) ), ivec3( 0 ), ivec3( u.textureDepths ).sub( 1 ) ); + const textureIdx = idx.z.mul( zStepSize ).add( idx.y.mul( yStepSize ) ).add( idx.x ); + const localCoord = fract( texCoord.mul( u.textureDepths ) ).toVar( 'localCoord' ); + // Scale up + localCoord.assign( fract( localCoord ) ); + const dispStrength = float( 0 ).toVar( 'dispStrength' ); + If( u.bivariate, () => { + dispStrength.assign( h.sample2ToOrder( localCoord, textureIdx, u.bivariateSelection ).r ); + } ).Else( () => { + dispStrength.assign( h.sample1( localCoord, textureIdx ) ); + } ); + const isnan = h.isNaNBits( dispStrength ).or( u.useF16.not().and( dispStrength.equal( 1. ) ) ); + If( isnan.not(), () => { + newPosition.addAssign( normal.mul( dispStrength.sub( u.displaceZero ) ).mul( u.displacement ) ); + } ) + }) + return newPosition; +}) + +export function createSphereMaterial(){ + const material = new THREE.NodeMaterial(); + material.transparent = true; + material.positionNode = sphereVertex(); + material.colorNode = Fn( () => { + const Color = vec4( 0 ).toVar( 'FinalColor' ); + const maskNBorders = maskAndBorder().toVar( 'early' ); + If( maskNBorders.a.greaterThanEqual( 0 ), () => { + Color.assign( maskNBorders ); + } ).Else( () => { + Color.assign( sphereColor() ); + } ); + return Color; + } )(); + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/sphereBlocks.ts b/src/components/textures/TSL/sphereBlocks.ts new file mode 100644 index 000000000..e1e2bbde1 --- /dev/null +++ b/src/components/textures/TSL/sphereBlocks.ts @@ -0,0 +1,142 @@ +import { + asin, atan, bool, clamp, cos, cross, float, Fn, + If, + instanceIndex, + mat3, mix, mul, normalize, PI, positionLocal, + positionWorld, select, sin, texture, + varying, + vec2, vec3, vec4 +} from 'three/tsl'; +import * as THREE from 'three/webgpu'; +import * as h from './utils/commonHelpers'; +import * as u from './utils/uniforms'; + +const giveMaskUV = Fn( ( [ position ] : [any] ) => { + const n = normalize( position ); + const latitude = asin( n.y ); + const longitude = atan( n.z, n.x ).negate(); + latitude.divAssign( PI ); + longitude.divAssign( mul( 2., PI ) ); + const u = longitude.add( 0.5 ); + const v = latitude.add( 0.5 ); + return vec2( u, v ); +}); + +const giveLonLat = Fn(([inUV] : [any])=>{ + const longitude = inUV.x.mul( u.lonBounds.y.sub( u.lonBounds.x ) ).add( u.lonBounds.x ); + const latitude = inUV.y.mul( u.latBounds.y.sub( u.latBounds.x ) ).add( u.latBounds.x ); + longitude.assign( longitude.negate() ); + return vec2( longitude, latitude ); +}) + +const givePosition = Fn(([lonlat] : [any])=>{ + const longitude = lonlat.x; + const latitude = lonlat.y; + // Convert to Cartesian coordinates + const cosLat = cos( latitude ).toVar(); + const x = cosLat.mul( cos( longitude ) ); + const y = sin( latitude ); + const z = cosLat.mul( sin( longitude ) ); + return vec4( x, y, z, cosLat ); +}) + +const getSpherePosition= Fn(([instanceUV] : [any]) =>{ + const lonlat = giveLonLat(instanceUV); + const sp = givePosition(lonlat); + return sp +}) + +const getOrientation = Fn(([spherePosition] : [any])=>{ + const normal = normalize(spherePosition); + const tangent = normalize(cross(vec3(0, 1, 0), normal)); + const bitangent = cross(normal, tangent); + const orientation = mat3(tangent, normal, bitangent); + return orientation +}) +const instanceColor = Fn(([strengths] : [any])=>{ + if (u.bivariate.value){ + const biCol = h.colorMixer( strengths.x, strengths.y ).toVar(); + return vec4(biCol.rgb, 1) + } else{ + const color = u.cmap.sample(vec2(strengths.x, 0.5)) + return vec4(color.rgb, 1) + } +}) + +const isBorder = Fn(()=>{ + const isBorder = bool(false).toVar(); + If(u.useBorderTexture, ()=>{ + const thisUV = giveMaskUV(positionWorld).toVar(); + //@ts-ignore .level does exist + const distance = u.borderTexture.sample(thisUV).level(0).r; + If(distance.lessThanEqual(u.borderWidth), () => { + isBorder.assign(bool(true)); + }); + }) + return isBorder; +}) + +export function createSphereBlocksMaterial(){ + const resolution = u.displacementResolution; + + const thisUV = Fn(()=>{ + const w = resolution.x; + const hi = resolution.y; + const px = instanceIndex.mod(w); + const py = instanceIndex.div(w).mod(hi); + const thisUV = vec2( + px.toFloat().add(0.5), + py.toFloat().add(0.5) + ).div(resolution) + return thisUV + })() + + const material = new THREE.NodeMaterial(); + material.depthWrite = true; + material.depthTest = true; + material.side = THREE.DoubleSide; + + // --- VERTEX --- // + const position = getSpherePosition(thisUV); + const tempUV = thisUV; + const newV = select(u.remapBorders, texture(u.remapTexture, h.realCoords(thisUV.xy)), tempUV); + const instanceUV = newV.toVar(); + const {localCoord, textureIdx} = h.getLocalCoord(instanceUV.xy) + let strength, biVal, isNan; + let colorIn; // what the fragment stage receives: vec2(first, second), unswapped for bivariate + if (u.bivariate.value){ + const raw = h.sample2(localCoord, textureIdx); + isNan = h.isNaNBits(raw.r).or(h.isNaNBits(raw.g)) + .or(u.useF16.not().and(raw.r.equal(1.0))) + .or(u.useF16.not().and(raw.g.equal(1.0))); + strength = mix(raw.r, raw.g, u.bivariateSelection) // For some reason select and Ifs don't work as expected on vertex/varying. Mix works though + biVal = mix(raw.g, raw.r, u.bivariateSelection) + colorIn = varying(raw, 'vBivar'); + } else { + strength = h.sample1(localCoord, textureIdx).toVar(); + strength = h.rescaler(strength); + isNan = h.isNaNBits(strength).or(u.useF16.not().and(strength.equal(1.0))); + strength = clamp(strength.mul(u.cScale).add(u.cOffset), 0.0, 0.995); + biVal = float(0); + colorIn = vec2(varying(strength, 'vStrength'), float(0)); + } + const heightFactor = strength.sub(u.displaceZero).mul(u.displacement); + const scaledPosition = vec3( + positionLocal.x.mul(position.w).mul(u.widthFactor), + positionLocal.y.mul(heightFactor), + positionLocal.z.mul(u.vertFactor), + ).toVar(); + const orientation = getOrientation(position.xyz); + const newPos = position.xyz.add(orientation.mul(scaledPosition)); + const clipped = strength.greaterThan(u.threshold.y) + .or(strength.lessThan(u.threshold.x)) + .or(isNan) + + const masked = h.maskOut(giveMaskUV(position.xyz)) + material.positionNode = select(clipped.or(masked), vec3(0), newPos); + + // --- Frag --- // + const borderHit = isBorder() + material.colorNode = select(borderHit, vec4(u.borderColor, 1), instanceColor(colorIn)); + return material; +} \ No newline at end of file diff --git a/src/components/textures/TSL/utils/commonHelpers.ts b/src/components/textures/TSL/utils/commonHelpers.ts new file mode 100644 index 000000000..246965692 --- /dev/null +++ b/src/components/textures/TSL/utils/commonHelpers.ts @@ -0,0 +1,222 @@ +import { + abs, + bool, + clamp, + float, + floatBitsToUint, + Fn, + fract, + glslFn, + If, + ivec2, ivec3, + max, + min, + mix, + round, + select, + texture, + uint, + vec2, vec3, + vec4, + wgslFn +} from 'three/tsl'; +import * as u from './uniforms'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; + + +// --- TEXTURE SAMPLERS ---// +const sampleMap = Fn(([p, index]: [any, any]) => { + const pv = p.toVar(); // This needs to be a variable for some reason + const result = vec4(0).toVar(); + u.map.forEach( ( tex, i ) => { + If( index.equal( i ), () => { + result.assign( tex.sample( pv ) ); + } ); + } ); + return result; + }); +export const sample1 = Fn(([p, index]: [any, any]) => sampleMap(p, index).r); +export const sample2 = Fn(([p, index]: [any, any]) => sampleMap(p, index).rg); + +export const sample2ToOrder = Fn(([p, index, variable]: [any, any, any]) => { + const biVar = sample2(p, index); + return select(variable.equal(0), biVar, biVar.gr); +}); + +export const getLocalCoord = (thisUV : any) =>{ + const zStepSize = uint(u.textureDepths.y).mul(uint(u.textureDepths.x)); + const yStepSize = uint(u.textureDepths.x); + const sampleCoord = thisUV.toVar(); + let textureIdx, localCoord; + if (u.isFlat.value) { + const idx = clamp(ivec2(sampleCoord.mul(u.textureDepths.xy)), ivec2(0), ivec2(u.textureDepths.xy).sub(1)); + textureIdx = idx.y.mul(yStepSize).add(idx.x); + localCoord = fract(sampleCoord.mul(u.textureDepths.xy)); + } else { + const texCoord = vec3(sampleCoord, u.animateProg); + const idx = clamp(ivec3(texCoord.mul(u.textureDepths)), ivec3(0), ivec3(u.textureDepths).sub(1)); + textureIdx = idx.z.mul(zStepSize).add(idx.y.mul(yStepSize)).add(idx.x); + localCoord = fract(texCoord.mul(u.textureDepths)); + } + return {localCoord, textureIdx} +} + + +// --- GEOHELPER --- // +export const realCoords = Fn(([uv]: [any]) => { + // Radians -> fraction of a full turn. Longitudes in -180..180 need shifting by 0.5. + const lonRange = u.lonBounds.div(2 * Math.PI); + const normalizedLon = select(u.is360, lonRange, lonRange.add(0.5)); + + // Latitude: -PI/2..PI/2 -> 0..1 + const normalizedLat = u.latBounds.div(Math.PI).add(0.5); + + const lonScale = normalizedLon.y.sub(normalizedLon.x); + const latScale = normalizedLat.y.sub(normalizedLat.x); + + const thisU = uv.x.mul(lonScale).add(normalizedLon.x); + const thisV = uv.y.mul(latScale).add(normalizedLat.x); + + return vec2(thisU, thisV); +}); + + +// --- BIVARIATE COLORS --- // +export const lerpColors = Fn(([A, B, fac]: [any, any, any]) => { + const steps = u.resolution.sub(1); + const snapped = round(fac.mul(steps)).div(steps); + return mix(A, B, snapped); +}); + +export const darkenColors = (A: any, B: any) => min(A, B); +export const lightenColors = (A: any, B: any) => max(A, B); +export const multiplyColors = (A: any, B: any) => clamp(A.mul(B), 0, 1); +export const differenceColors = (A: any, B: any) => abs(A.sub(B)); + +export const colorMixer = Fn(([A, B]: [any, any]) => { + const bottomColor = lerpColors(u.bottomLeft, u.bottomRight, A); + const leftColor = lerpColors(u.bottomLeft, u.topLeft, B); + + // Pick the blend mode (falls back to darken) + return select( + u.mixMode.equal(1), lightenColors(bottomColor, leftColor), + select( + u.mixMode.equal(2), multiplyColors(bottomColor, leftColor), + select( + u.mixMode.equal(3), differenceColors(bottomColor, leftColor), + darkenColors(bottomColor, leftColor), + ), + ), + ); +}); + +export function createBivariateColor() { + return Fn(([p, index]: [any, any]) => { + const biValues = sample2(p, index); + const hasNaN = isNaNBits(biValues.r).or(isNaNBits(biValues.g)); + + const color = select(hasNaN, vec3(0, 0, 0), colorMixer(biValues.r, biValues.g)); + return vec4(color, select(hasNaN, float(1), float(0))); + }); +} + + +// --- VALUE SCALING --- // +export const denorm = (x: any) => x.mul(u.valueRange.y.sub(u.valueRange.x)).add(u.valueRange.x); +export const norm = (x: any) => x.sub(u.valueRange.x).div(u.valueRange.y.sub(u.valueRange.x)); + + +const makeColorScale = (expr: string) => { + if ( u.isWebGPU.value ) return wgslFn(` + fn applyColorScale( x: f32 ) -> f32 { + return ${expr}; + } + `) + return glslFn(` + float rescaler(float x){ + return ${expr}; + } + `) +}; + +export const rescaler = Fn(([x]: [any]) => { + const result = x.toVar(); + If(u.rescaleValues, () => { + const applyColorScale = makeColorScale(usePlotStore.getState().colorScale?? "x"); + result.assign(norm(applyColorScale({ x: denorm(x) }))); + }); + return result; +}); + +export const reprojector = Fn(([texCoord, maskUV] : [any, any]) => { + const valid = bool(true).toVar() + If(u.reproject, () => { + // Each output pixel looks up where it comes from in the source data + const remap = texture(u.remapTexture, texCoord.xy).rgb.toVar(); + texCoord.assign(vec3(remap.rg, texCoord.z)); + maskUV.assign(realCoords(remap.rg)); + valid.assign(remap.b.greaterThan(0.5)) + }).Else(()=>{ + maskUV.assign(realCoords(texCoord.xy)); + }) + If(u.remapBorders,()=> { + const tempV = texCoord.xy.toVar(); + tempV.y.assign(select(u.flipY, tempV.y.oneMinus(), tempV.y)); + maskUV.assign(u.remapBorderTexture.sample(tempV).ba) + }) + return valid +}) + +export const reprojectMask = Fn(([texCoord, dda=bool(false)] : [any, any]) => { + const maskUV = texCoord.toVar(); + const remap = texCoord.toVar(); + If(u.reproject, () => { + // Each output pixel looks up where it comes from in the source data + remap.assign(texture(u.remapTexture, texCoord.xy).rgb.toVar()); + maskUV.assign(realCoords(select(dda, remap, texCoord.xy))); + }).Else(()=>{ + maskUV.assign(realCoords(texCoord.xy)); + }) + If(u.remapBorders,()=> { + const tempV = select(dda, remap, texCoord.xy).toVar(); + tempV.y.assign(select(u.flipY, tempV.y.oneMinus(), tempV.y)); + maskUV.assign(u.remapBorderTexture.sample(tempV).ba) + }) + // For 0–360 data, wrap longitude by half a turn + maskUV.x.assign(select(u.is360, + fract(maskUV.x.add(0.5)), + maskUV.x + )); + return maskUV +}) + +export const isBorder = Fn(([thisUV] : [any])=>{ + // Takes maskUV from reprojector or ___ Coords. Assumes already been transformed + const isBorder = bool(false).toVar(); + If(u.useBorderTexture, ()=>{ + //@ts-ignore .level does exist + const distance = u.borderTexture.sample(thisUV).level(0).r; + If(distance.lessThanEqual(u.borderWidth), () => { + isBorder.assign(bool(true)); + }); + }) + return isBorder; +}) + +export const maskOut = Fn(([inUV] : [any])=>{ + const remove = bool(false).toVar(); + If(u.maskValue.notEqual(0), () => { + const mask = u.maskTexture.sample(inUV).r.toVar(); + const masked = select(u.maskValue.equal(1), mask.lessThan(0.5), mask.greaterThanEqual(0.5)); + If(masked, () => { remove.assign(bool(true)); }); + }); + return remove +}) + +// --- NANNERS ---// +export const isNaNBits = Fn(([x]: [any]) => { + const bits = floatBitsToUint(x); + const exponent = bits.bitAnd(uint(0x7f800000)); + const mantissa = bits.bitAnd(uint(0x007fffff)); + return exponent.equal(uint(0x7f800000)).and(mantissa.notEqual(uint(0))); +}); \ No newline at end of file diff --git a/src/components/textures/TSL/utils/uniformUpdater.tsx b/src/components/textures/TSL/utils/uniformUpdater.tsx new file mode 100644 index 000000000..dde05dc4e --- /dev/null +++ b/src/components/textures/TSL/utils/uniformUpdater.tsx @@ -0,0 +1,153 @@ +import { useGlobalStore } from '@/GlobalStates/GlobalStore' +import { usePlotStore } from '@/GlobalStates/PlotStore' +import { useColormapStore } from '@/GlobalStates/ColormapStore' +import { useEffect, useRef } from 'react' +import * as THREE from 'three' +import { useShallow } from 'zustand/shallow' +import { useCoordBounds } from '@/hooks/useCoordBounds' +import * as u from '@/components/textures/TSL/utils/uniforms' +import { useValueScales } from '@/hooks' + +export function uniformUpdater(){ + const {cScale, cOffset, animProg, nanTransparency, nanColor, fillValue, maskValue, valueRange, + useBorderTexture, borderColor, borderWidth, is360Deg, showBorders, interpPixels, colorScale, + xRange, yRange, zRange, quality, displacement, offsetNegatives, scalePoints, + pointSize, scaleIntensity, timeScale, plotType} = usePlotStore(useShallow(s => ({ + cScale: s.cScale, cOffset: s.cOffset, animProg: s.animProg, nanTransparency: s.nanTransparency, nanColor: s.nanColor, fillValue: s.fillValue, maskValue: s.maskValue, valueRange: s.valueRange, + useBorderTexture: s.useBorderTexture, borderColor: s.borderColor, borderWidth: s.borderWidth, is360Deg: s.is360Deg, showBorders: s.showBorders, interpPixels: s.interpPixels, colorScale: s.colorScale, + xRange: s.xRange, yRange: s.yRange, zRange: s.zRange, quality: s.quality, displacement: s.displacement, offsetNegatives: s.offsetNegatives, scalePoints: s.scalePoints, + pointSize: s.pointSize, scaleIntensity: s.scaleIntensity, timeScale: s.timeScale, plotType: s.plotType + }))) + const { useF16Textures, bivariate, textureArrayDepths, remapTexture, isWebGPU, + flipY, remapBorders, isFlat, shape, dataShape, mainTextures} = useGlobalStore(useShallow(s => ({ + useF16Textures: s.useF16Textures, bivariate: s.bivariate, textureArrayDepths: s.textureArrayDepths, remapTexture: s.remapTexture, isWebGPU: s.isWebGPU, + flipY: s.flipY, remapBorders: s.remapBorders, isFlat: s.isFlat, shape: s.shape, dataShape: s.dataShape, mainTextures: s.mainTextures + }))) + const {lonBounds, latBounds} = useCoordBounds() + const {colormap, bottomLeft, bottomRight, topLeft, resolution, mixMode, bivariateSelection} = useColormapStore(useShallow(s => ({ + colormap: s.colormap, bottomLeft: s.bottomLeft, bottomRight: s.bottomRight, topLeft: s.topLeft, resolution: s.resolution, mixMode: s.mixMode, bivariateSelection: s.bivariateSelection + }))) + const valueScales = useValueScales(); + // --- RENDERER ---// + useEffect(()=>{ + u.isWebGPU.value = isWebGPU + },[isWebGPU]) + // --- COLORS --- // + useEffect(() => { + u.nanColor.value = new THREE.Color(nanColor).convertLinearToSRGB(); + u.borderColor.value = new THREE.Color(borderColor).convertLinearToSRGB(); + }, [nanColor, borderColor]) + // --- COLORMAP --- // + useEffect(()=>{ + u.cmap.value = colormap; + u.cmap.value.colorSpace = THREE.NoColorSpace; + u.cmap.value.needsUpdate = true; + u.bottomLeft.value = new THREE.Color(bottomLeft).convertLinearToSRGB(); + u.bottomRight.value = new THREE.Color(bottomRight).convertLinearToSRGB(); + u.topLeft.value = new THREE.Color(topLeft).convertLinearToSRGB(); + u.resolution.value = resolution; + u.mixMode.value = mixMode; + }, [colormap, bottomLeft, bottomRight, topLeft, resolution, mixMode]) + // ---ANIMATION --- // + useEffect(()=>{ + u.animateProg.value = animProg; + }, [animProg]) + // --- REPROJECTION --- // + useEffect(()=>{ + u.remapTexture.value = remapTexture?? new THREE.DataTexture(); + u.reproject.value = Boolean(remapTexture); + },[remapTexture]) + useEffect(()=>{ + u.remapBorderTexture.value = remapBorders?? new THREE.DataTexture(); + u.remapBorders.value = Boolean(remapBorders) + },[remapBorders]) + // --- BORDERS --- // + useEffect(()=>{ + u.trim.value = plotType != 'sphere' + },[plotType]) + // --- GENERAL --- // + useEffect(()=>{ + u.cOffset.value = cOffset; + u.nanAlpha.value = 1 - nanTransparency; + u.cScale.value = cScale; + u.threshold.value.set(valueRange[0], valueRange[1]); + u.latBounds.value.set(latBounds[0], latBounds[1]); + u.lonBounds.value.set(lonBounds[0], lonBounds[1]); + u.maskValue.value = maskValue; + u.fillValue.value = fillValue?? NaN; + u.useBorderTexture.value = useBorderTexture && showBorders; + u.borderWidth.value = borderWidth; + u.is360.value = is360Deg; + u.textureDepths.value.set(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0]); + u.valueRange.value.set(valueScales.minVal, valueScales.maxVal); + u.useF16.value = useF16Textures; + u.bivariateSelection.value = bivariateSelection; + u.bivariate.value = bivariate; + u.isFlat.value = isFlat + },[ + cScale, cOffset, nanTransparency, fillValue, maskValue, valueRange, + lonBounds, latBounds, useBorderTexture, borderWidth, is360Deg, showBorders, + valueScales, useF16Textures, bivariateSelection, + bivariate, textureArrayDepths, isFlat + ]) + // --- BLOCKS --- // + useEffect(()=>{ + u.flipY.value = flipY; + }, [flipY]) + // --- INTERP PIXELS --- // + useEffect(()=>{ + const nodes = u.map + const filter = interpPixels ? THREE.LinearFilter : THREE.NearestFilter + for (let i = 0; i < nodes.length; i++){ + const tex = nodes[i].value.clone() + nodes[i].value.dispose(); + tex.minFilter = filter; + tex.magFilter = filter; + tex.needsUpdate = true; + u.map[i].value = tex; + } + },[interpPixels]) + // --- TEXTURES --- // + const prev = useRef(undefined) + useEffect(()=>{ + if (mainTextures === prev.current) return; + prev.current = mainTextures; + mainTextures && u.setMapTextures(mainTextures); + }) + // --- COLORSCALE --- // + useEffect(()=>{ + u.rescaleValues.value = Boolean(colorScale) + },[colorScale]) + + // --- VOLUME UNIFORMS ---// + useEffect(()=>{ + if (plotType == 'point-cloud') return; + const aspectRatio = shape.y/shape.x + const timeRatio = shape.z/shape.x; + u.volumeScale.value = shape; + u.flatBounds.value.set(-xRange[1],-xRange[0],zRange[0] * timeRatio, zRange[1] * timeRatio); + u.vertBounds.value.set( + (flipY ? -yRange[1] : yRange[0]) * aspectRatio, + (flipY ? -yRange[0] : yRange[1]) * aspectRatio, + ); + u.dataShape.value = remapTexture + ? new THREE.Vector3(remapTexture.image.width, remapTexture.image.height, dataShape[0]) + : new THREE.Vector3(dataShape[2], dataShape[1], dataShape[0]) + u.steps.value = quality; + },[shape, xRange, yRange, zRange, dataShape, quality, flipY, remapTexture]) + + // --- DISPLACEMENT UNIFORMS ---// + useEffect(()=>{ + u.displacement.value = displacement + u.displaceZero.value = offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) + u.widthFactor.value = Math.abs(lonBounds[1]-lonBounds[0])/(2.0*Math.PI) + u.vertFactor.value = Math.abs(latBounds[1]-latBounds[0])/(Math.PI) + },[valueScales, displacement, offsetNegatives, lonBounds, latBounds]) + // --- POINT UNIFORMS ---// + useEffect(()=>{ + u.pointSize.value = pointSize; + u.scaleIntensity.value = scaleIntensity; + u.timeScale.value = timeScale; + u.scaleByVal.value = scalePoints + },[pointSize, scaleIntensity, timeScale, scalePoints]) +} \ No newline at end of file diff --git a/src/components/textures/TSL/utils/uniforms.ts b/src/components/textures/TSL/utils/uniforms.ts new file mode 100644 index 000000000..f688daba1 --- /dev/null +++ b/src/components/textures/TSL/utils/uniforms.ts @@ -0,0 +1,97 @@ +import * as THREE from 'three/webgpu' +import { Data3DTexture, DataTexture, Texture } from 'three/webgpu'; +import { texture, uniform, bool, float, int, vec2, vec3, vec4, color, texture3D } from 'three/tsl'; +import { useGlobalStore } from '@/GlobalStates/GlobalStore'; + +const placeholder3D = () => { + const t = new THREE.Data3DTexture(new Uint8Array([0, 0, 0, 255]), 1, 1, 1); + t.format = THREE.RGBAFormat; t.type = THREE.UnsignedByteType; + t.minFilter = t.magFilter = THREE.LinearFilter; + t.wrapS = t.wrapT = t.wrapR = THREE.ClampToEdgeWrapping; + t.needsUpdate = true; + return t; +}; + +const MAX_MAPS = 8 +export let map: any[] = Array.from({ length: MAX_MAPS }, () => + texture3D(placeholder3D()) +); + +export function setMapTextures(textures: (DataTexture | Data3DTexture | Texture)[]) { + if (!textures || !textures.length) return; + const is3D = !!(textures[0] as any).isData3DTexture; + const kindChanged = is3D !== !!(map[0].value as any).isData3DTexture; + const countChanged = textures.length !== map.length; + + if (kindChanged || countChanged) { + // New nodes: materials rebuild against these when they observe mapVersion + map = textures.map(t => is3D ? texture3D(t as Data3DTexture) : texture(t as DataTexture)); + } else { + // Same kind & count: hot-swap bindings, no recompile needed + map.forEach((node, i) => { + if (node.value !== textures[i]) { + node.value = textures[i]; + textures[i].needsUpdate = true; + } + }); + } +} +// --- GENERAL --- // +export const maskTexture = texture( new DataTexture() ); +export const cmap = texture( new DataTexture() ); +export const remapTexture = texture( new DataTexture() ); +export const reproject = uniform( bool(false) ); +export const borderTexture = texture( new Texture() ); +export const remapBorders = uniform( bool() ); +export const remapBorderTexture = texture( new DataTexture() ); +export const useBorderTexture = uniform( bool() ); +export const borderWidth = uniform( float() ); +export const borderColor = uniform( color() ); +export const textureDepths = uniform( vec3() ); +export const bivariate = uniform( bool() ); +export const bottomLeft = uniform( color() ); +export const topLeft = uniform( color() ); +export const bottomRight = uniform( color() ); +export const resolution = uniform( float() ); +export const mixMode = uniform( int() ); +export const bivariateSelection = uniform( int() ); +export const is360 = uniform( bool() ); +export const cOffset = uniform( float() ); +export const cScale = uniform( float() ); +export const threshold = uniform( vec2() ); +export const animateProg = uniform( float() ); +export const nanAlpha = uniform( float() ); +export const nanColor = uniform( color() ); +export const maskValue = uniform( int() ); +export const fillValue = uniform( float() ); +export const latBounds = uniform( vec2() ); +export const lonBounds = uniform( vec2() ); +export const valueRange = uniform( vec2() ); +export const useF16 = uniform( bool() ); +export const isFlat = uniform( bool() ); +export const flipY = uniform(bool(false)); +export const rescaleValues = uniform(bool(false)); +export const isWebGPU = uniform(bool(true)); +export const trim = uniform(bool(false)); + +// --- VOLUME UNIFORMS --- // +export const volumeScale = uniform(vec3()); +export const steps = uniform(int()); +export const flatBounds = uniform(vec4()); +export const vertBounds = uniform(vec2()); +export const dataShape = uniform(vec3()) +export const tfLUT = texture( new DataTexture() ); + +// --- BLOCK UNIFORMS --- // +export const displacement = uniform(float(2)); +export const displaceZero = uniform(float(0)); +export const displacementResolution = uniform(vec2(0.0)) +export const widthFactor = uniform(1), vertFactor = uniform(1); + +// --- POINT UNIFORMS --- // +export const pointShape = uniform(vec3(0)) +export const globalScale = uniform(float(0)) +export const pointSize = uniform(float(0)) +export const timeScale = uniform(float(0)) +export const scaleByVal = uniform(bool(false)) +export const scaleIntensity = uniform(float(0)) diff --git a/src/components/textures/TSL/utils/volumeHelpers.ts b/src/components/textures/TSL/utils/volumeHelpers.ts new file mode 100644 index 000000000..6da334bb9 --- /dev/null +++ b/src/components/textures/TSL/utils/volumeHelpers.ts @@ -0,0 +1,134 @@ +import { + abs, + any,bool,clamp,EPSILON,float,Fn,fract,If,int,ivec3, + max,min,mod,pow,select,vec2,vec3 +} from 'three/tsl'; +import * as h from './commonHelpers'; +import * as u from './uniforms'; +import * as THREE from 'three/webgpu' + +export const shouldSkip = (p: any, texCoord: any, maskUV: any) => { + const skip = bool(false).toVar(); + const outOfBounds = p.x.greaterThan(u.flatBounds.x.negate()) + .or(p.x.lessThan(u.flatBounds.y.negate())) + .or(p.z.negate().greaterThan(u.flatBounds.z.negate())) + .or(p.z.negate().lessThan(u.flatBounds.w.negate())) + .or(p.y.lessThan(u.vertBounds.x)) + .or(p.y.greaterThan(u.vertBounds.y)); + If(outOfBounds, () => { skip.assign(bool(true)); }); + + If(skip.not(), () => { + texCoord.assign(p.div(u.volumeScale).add(0.5)); + const valid = h.reprojector(texCoord, maskUV); + If(valid.not(), () => { skip.assign(bool(true)); }); + }); + + If(skip.not(), () => { + If(any(maskUV.greaterThan(vec2(1.0))).or(any(maskUV.lessThan(vec2(0.0)))), () => { + skip.assign(bool(true)); + }); + }); + + If(skip.not().and(u.maskValue.notEqual(0)), () => { + const mask = u.maskTexture.sample(maskUV).r; + const masked = select(u.maskValue.equal(1), mask.lessThan(0.5), mask.greaterThanEqual(0.5)); + If(masked, () => { skip.assign(bool(true)); }); + }); + + If(skip.not(), () => { + texCoord.z.assign(mod(texCoord.z.add(u.animateProg), 1.0001)); + texCoord.assign(clamp(texCoord, vec3(0.0), vec3(1.0).sub(vec3(EPSILON)))); + }); + + return skip; +}; + + +export const sampleVoxel = (texCoord: any, d: any, biVal: any, isNan: any) => { + // This gets the sample value. If d is clipped by value-range return false + const depths = ivec3(u.textureDepths); + const yStepSize = depths.x; + const zStepSize = depths.y.mul(depths.x); + const idx = clamp(ivec3(texCoord.mul(u.textureDepths)), ivec3(0), depths.sub(int(1))); + const textureIdx = idx.z.mul(zStepSize).add(idx.y.mul(yStepSize)).add(idx.x); + const localCoord = fract(texCoord.mul(u.textureDepths)); + + If(u.bivariate, () => { + const bivar = h.sample2ToOrder(localCoord, textureIdx, u.bivariateSelection).toVar(); + d.assign(bivar.r); + biVal.assign(bivar.g); + isNan.assign( + h.isNaNBits(d).or(h.isNaNBits(biVal)) + .or(u.useF16.not().and(d.equal(1.0))) + .or(u.useF16.not().and(biVal.equal(1.0))) + ); + }).Else(() => { + d.assign(h.sample1(localCoord, textureIdx)); + d.assign(h.rescaler(d)); // GLSL rescaler(inout d) -> returns the rescaled value here + isNan.assign(h.isNaNBits(d).or(u.useF16.not().and(d.equal(1.0)))); + d.assign(max(min(d.mul(u.cScale).add(u.cOffset), 0.995), 0.0)); + }); + + return d.greaterThanEqual(u.threshold.x).and(d.lessThanEqual(u.threshold.y)).toVar(); +}; + +export const colorTexel = (d: any, biVal: any, isNan: any, accumColor: any, alphaAcc: any ) => { + isNan.assign(isNan.or(abs(d.sub(u.fillValue)).lessThan(0.005))); + If(isNan, () => { + If(u.nanAlpha.greaterThan(0.0), () => { + const nanA = pow(u.nanAlpha, 5.0); + accumColor.addAssign(float(1.0).sub(alphaAcc).mul(nanA).mul(u.nanColor)); + alphaAcc.addAssign(nanA); + }); + }).Else(() => { + const s = u.tfLUT.sample(vec2(d, 0.5)).toVar(); + const opacity = float(1).sub(alphaAcc).toVar(); + If( u.bivariate, () => { + const flipOrder = u.bivariateSelection.notEqual( 0 ); + const biCol = select( flipOrder, h.colorMixer( biVal, d ), h.colorMixer( d, biVal ) ).toVar(); + accumColor.addAssign(opacity.mul(s.a).mul(biCol.rgb)); + }).Else( () => { + accumColor.addAssign(opacity.mul(s.rgb)); + }) + alphaAcc.addAssign(opacity.mul(s.a)); + }); +} + +export const hitBox = Fn(([orig, dir]: any[]) => { + const boxMax = u.volumeScale.mul(0.5); + const boxMin = boxMax.negate(); + const invDir = vec3(1.0).div(dir); + const tA = boxMin.sub(orig).mul(invDir); + const tB = boxMax.sub(orig).mul(invDir); + const tmin = min(tA, tB); + const tmax = max(tA, tB); + const t0 = max(tmin.x, max(tmin.y, tmin.z)); + const t1 = min(tmax.x, min(tmax.y, tmax.z)); + return vec2(t0, t1); +}); + +export function buildTransferLUT(p: { + cmap: (d: number) => [number, number, number]; + threshold: [number, number]; + useClipScale: boolean; + revTransparency: boolean; + transparency: number; + opacityMag: number; +}, size = 256) { + const data = new Uint16Array(size * 4); // half float + const exp = p.transparency * p.opacityMag; + const [t0, t1] = p.threshold; + for (let i = 0; i < size; i++) { + const d = i / (size - 1); + const af = p.revTransparency ? 1 - d : d; + const n = p.useClipScale ? Math.min(Math.max((af - t0) / (t1 - t0), 0), 1) : af; + const a = Math.pow(Math.max(n, 0.001), exp); + const [r, g, b] = p.cmap(d); + const newCol = new THREE.Color(r,g,b).convertLinearToSRGB() + data.set([newCol.r * a, newCol.g * a, newCol.b * a, a].map(THREE.DataUtils.toHalfFloat), i * 4); // premultiplied + } + const tex = new THREE.DataTexture(data, size, 1, THREE.RGBAFormat, THREE.HalfFloatType); + tex.minFilter = tex.magFilter = THREE.LinearFilter; + tex.needsUpdate = true; + return tex; +} \ No newline at end of file diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 305d6ff69..a25fe931b 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -1,5 +1,6 @@ import * as THREE from 'three'; import { colorschemes, get, findColorScheme } from 'color-schemes-js'; +import { lerp } from '@/utils/colorUtils'; export const colormaps = ['magma', 'inferno', 'plasma', 'viridis', 'cividis', 'twilight', 'twilight_shifted', 'turbo', 'Blues', 'BrBG', 'BuGn', 'BuPu', 'CMRmap', 'GnBu', 'Greens', 'Greys', 'OrRd', 'Oranges', 'PRGn', 'PiYG', 'PuBu', 'PuBuGn', 'PuOr', 'PuRd', 'Purples', 'RdBu', 'RdGy', 'RdPu', 'RdYlBu', 'RdYlGn', 'Reds', 'Spectral', 'Wistia', 'YlGn', 'YlGnBu', 'YlOrBr', 'YlOrRd', 'afmhot', 'autumn', 'binary', 'bone', 'brg', 'bwr', 'cool', 'coolwarm', 'copper', 'cubehelix', 'flag', 'gist_earth', 'gist_gray', 'gist_heat', 'gist_ncar', 'gist_rainbow', 'gist_stern', 'gist_yarg', 'gnuplot', 'gnuplot2', 'gray', 'hot', 'hsv', 'jet', 'nipy_spectral', 'ocean', 'pink', 'prism', 'rainbow', 'seismic', 'spring', 'summer', 'terrain', 'winter', 'Accent', 'Dark2', 'Paired', 'Pastel1', 'Pastel2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c']; @@ -77,6 +78,26 @@ export function evaluateColorMap(value: number, palette: string, reverse = false return [rgb.r, rgb.g, rgb.b]; } +export function sampleColormap(value: number, texture: THREE.DataTexture): [number, number, number]{ + const imageData = texture.source.data; + const {width, data} = imageData + if (!data) return [0,0,0] + const t = Math.min(Math.max(value, 0), 1); + const fac = t * (width - 1); + const facIdx = Math.min(Math.floor(fac), width - 2); + const mixVal = fac - facIdx; + const lo = facIdx * 4; + const hi = (facIdx + 1) * 4; + const r = THREE.MathUtils.lerp(data[lo], data[hi], mixVal) / 255; + const g = THREE.MathUtils.lerp(data[lo + 1], data[hi + 1], mixVal) / 255; + const b = THREE.MathUtils.lerp(data[lo + 2], data[hi + 2], mixVal) / 255; + const c = new THREE.Color(); + // r,g,b are 0-1 sRGB values from the colormap (divide by 255 if they're bytes) + c.setRGB(r, g, b, THREE.SRGBColorSpace); // converts into the working (linear) space + return [c.r, c.g, c.b]; +} + + export function getColormapGradientCss(name: string): string { const schemeName = resolveColorSchemeName(name); const scheme = colorschemes[schemeName]; @@ -124,6 +145,7 @@ export function GetColorMapTexture( } // Create a new texture if not already available const newTexture = new THREE.DataTexture(colData, rgbv.length + 1, 1, THREE.RGBAFormat); + newTexture.colorSpace = THREE.NoColorSpace newTexture.needsUpdate = true; return newTexture; } diff --git a/src/components/textures/shaders/bordersFrag.glsl b/src/components/textures/shaders/bordersFrag.glsl index 55f55ca86..1680c4bee 100644 --- a/src/components/textures/shaders/bordersFrag.glsl +++ b/src/components/textures/shaders/bordersFrag.glsl @@ -12,9 +12,14 @@ uniform bool trim; void main() { - if ((aPosition.x < xBounds.x || aPosition.x > xBounds.y || aPosition.y < yBounds.x || aPosition.y > yBounds.y) && trim){ - discard; - } + If( aPosition.x.lessThan( xBounds.x ) + .or( aPosition.x.greaterThan( xBounds.y ) ) + .or( aPosition.y.lessThan( yBounds.x ) ) + .or( aPosition.y.greaterThan( yBounds.y ) ) + .and( trim ), () => { + Discard(); + +} ); color = vec4(borderColor, 1.0); } \ No newline at end of file diff --git a/src/components/ui/Elements/ColorAdjuster.tsx b/src/components/ui/Elements/ColorAdjuster.tsx index 97ce175e1..95db913c5 100644 --- a/src/components/ui/Elements/ColorAdjuster.tsx +++ b/src/components/ui/Elements/ColorAdjuster.tsx @@ -7,11 +7,23 @@ import { useShallow } from 'zustand/shallow' import { usePlotStore } from '@/GlobalStates/PlotStore' import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { useGlobalStore } from '@/GlobalStates/GlobalStore' + +const sharedFunctions = [ + 'abs(x)', 'sqrt(x)', 'min(x, y)', 'max(x, y)', + 'log(x)', 'log2(x)', 'exp(x)', 'exp2(x)', + 'pow(x, y)', 'floor(x)', 'ceil(x)', 'fract(x)', + 'round(x)', 'clamp(x, a, b)' +] + const ColorAdjuster = () => { const customShader = useRef(''); - const {setError, setCustomError} = useErrorStore(useShallow(s => s)) - const {colorScale} = usePlotStore(useShallow( s=>s)) - const {useF16Textures, setUseF16Textures, setStatus} = useGlobalStore(useShallow(s => s)) + const {setError, setCustomError} = useErrorStore(useShallow(s => ({ + setError: s.setError, setCustomError: s.setCustomError + }))) + const colorScale = usePlotStore(s => s.colorScale) + const {useF16Textures, isWebGPU, setUseF16Textures, setStatus} = useGlobalStore(useShallow(s => ({ + useF16Textures: s.useF16Textures, isWebGPU: s.isWebGPU, setUseF16Textures: s.setUseF16Textures, setStatus: s.setStatus + }))) const handleShader = () =>{ const shader = customShader.current const {ok, log} = glslValidator(shader) @@ -26,10 +38,9 @@ const ColorAdjuster = () => { usePlotStore.setState({colorScale:undefined}); return; } - const glslString = `x = ${shader};` + const glslString = `${shader}` usePlotStore.setState({colorScale:glslString}) } - return (
{colorScale @@ -56,13 +67,7 @@ const ColorAdjuster = () => {

Along with general arithmetic operations. The following globals can also be used:

    - {[ - 'abs(x)', 'sqrt(x)', 'min(x, y)', 'max(x, y)', - 'log(x)', 'log2(x)', 'exp(x)', 'exp2(x)', - 'pow(x, y)', 'floor(x)', 'ceil(x)', ,'mod(x, y)', - 'round(x)', 'mix(x, y, a)', - 'fract(x)', 'inverseqrt(x)', - ].map((fn)=> ( + {sharedFunctions.map((fn)=> (
  • {fn}
  • ))}
diff --git a/src/components/ui/Elements/HomeButton.tsx b/src/components/ui/Elements/HomeButton.tsx index 4900c46f6..29cb8f45c 100644 --- a/src/components/ui/Elements/HomeButton.tsx +++ b/src/components/ui/Elements/HomeButton.tsx @@ -1,5 +1,4 @@ "use client"; - import Image from "next/image"; import Link from "next/link"; import { Button } from "@/components/ui/button-enhanced"; @@ -9,7 +8,7 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; -const logoHome = "/logo-light.svg"; +const logoHome = "./bz_logo.svg"; export default function HomeButton() { return ( @@ -21,7 +20,7 @@ export default function HomeButton() { size="icon" className="cursor-pointer hover:scale-90 transition-transform duration-100 ease-out" > - logoMPI + logoMPI diff --git a/src/components/ui/Elements/WebGPUPlacard.tsx b/src/components/ui/Elements/WebGPUPlacard.tsx new file mode 100644 index 000000000..bf4d6c131 --- /dev/null +++ b/src/components/ui/Elements/WebGPUPlacard.tsx @@ -0,0 +1,88 @@ +import { useGlobalStore } from '@/GlobalStates/GlobalStore' +import React, { useState } from 'react' + +const LABELS: Record = { + webgpu: { + text: "WebGPU enabled", + hint: "Rendering with WebGPU", + }, + webgl: { + text: "Using WebGL", + hint: "WebGPU isn't available in this browser, so WebGL is rendering instead", + }, +}; +export async function detectGpuBackend() { + try { + const gpu = (navigator as any).gpu; + if (gpu) { + const adapter = await gpu.requestAdapter(); + if (adapter) return "webgpu"; + } + } catch { + /* fall through to WebGL */ + } + return "webgl"; +} + +export const WebGPUPlacard = () => { + const [active,setActive] = useState('webgpu') + const { text, hint } = LABELS[active]; + detectGpuBackend().then(e=>setActive(e)) + return ( + <> + + + + + ) +} + +const css = ` +.gpu-badge { + --gb-fg: #1f4a44; + --gb-bg: rgba(63, 130, 120, 0.06); + --gb-border: rgba(63, 130, 120, 0.07); + --gb-dot: #2f9a8a; + position: absolute; + left:50%; + top: 2%; + transform:translatex(-50%); + display: inline-flex; + align-items: center; + gap: 0.6rem; + padding: 0.4rem 0.95rem 0.4rem 0.8rem; + border: 1px solid var(--gb-border); + border-radius: 999px; + background: var(--gb-bg); + color: var(--text-paragraph); + box-shadow: 0 1px 2px var(--modal-shadow), + 0 4px 18px -4px color-mix(in srgb, var(--gb-dot) 35%, transparent); + font: 600 0.875rem/1.2 system-ui, -apple-system, "Segoe UI", sans-serif; + letter-spacing: 0.01em; + white-space: nowrap; + cursor: default; + user-select: none; +} + +.gpu-badge--webgl { + --gb-fg: #5a4a2a; + --gb-bg: rgba(160, 128, 70, 0.15); + --gb-border: rgba(160, 128, 70, 0.45); + --gb-dot: #b8903f; +} + +.gpu-badge__dot { + width: 0.55rem; + height: 0.55rem; + border-radius: 50%; + background: var(--gb-dot); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--gb-dot) 24%, transparent); +} + +`; \ No newline at end of file diff --git a/src/components/ui/MainPanel/AdjustPlot.tsx b/src/components/ui/MainPanel/AdjustPlot.tsx index b2457b25d..3677b453c 100644 --- a/src/components/ui/MainPanel/AdjustPlot.tsx +++ b/src/components/ui/MainPanel/AdjustPlot.tsx @@ -213,9 +213,9 @@ const PointOptions = () =>{

Point Size

setPointSize(vals[0])} @@ -276,7 +276,7 @@ const FlatOptions = () =>{ step={2} value={[displacement]} className='w-full mb-2' - onValueChange={(vals:number[]) => (setDisplacement(vals[0]))} + onValueChange={(vals:number[]) => {setDisplacement(vals[0])}} />
@@ -464,7 +464,7 @@ const GlobalOptions = () =>{ } onClick={() => setShowBorders(!showBorders)}>{showBorders ? "Hide Borders" : "Show Borders" } - + usePlotStore.setState({useBorderTexture:!useBorderTexture}) } /> diff --git a/src/components/ui/MainPanel/PlotType.tsx b/src/components/ui/MainPanel/PlotType.tsx index 2f593dfc1..9c848dc2a 100644 --- a/src/components/ui/MainPanel/PlotType.tsx +++ b/src/components/ui/MainPanel/PlotType.tsx @@ -26,8 +26,12 @@ const plotIcons = { } const PlotType = () => { - const { plotType, setPlotType, setUseOrtho } = usePlotStore(useShallow(s => s)) - const {isFlat, variable} = useGlobalStore(useShallow(s => s)) + const { plotType, setPlotType, setUseOrtho } = usePlotStore(useShallow(s => ({ + plotType: s.plotType, setPlotType: s.setPlotType, setUseOrtho: s.setUseOrtho + }))) + const {isFlat, variable} = useGlobalStore(useShallow(s => ({ + isFlat: s.isFlat, variable: s.variable + }))) // Responsive popover side const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); const {dataShape, is4D} = useGlobalStore(useShallow(s => s)) diff --git a/src/components/zarr/icechunk-store.ts b/src/components/zarr/icechunk-store.ts index ce6efe608..d0d8bdd9b 100644 --- a/src/components/zarr/icechunk-store.ts +++ b/src/components/zarr/icechunk-store.ts @@ -50,15 +50,6 @@ export async function getIcechunkStore( ...(options?.snapshot && { snapshot: options.snapshot }), ...(fetchClient && { fetchClient }), }); - // console.log(icechunkStore); - // const storage = new HttpStorage(storePath); - // console.log('Storage initialized:', storage); - // const repo = await Repository.open({ storage }); - // console.log('Repository opened:', repo); - // const branches = await repo.listBranches(); - // const tags = await repo.listTags(); - // console.log('Branches:', branches); - // console.log('Tags:', tags); // Prime the node cache immediately after opening (icechunkStore as any)._cachedNodes = icechunkStore.listNodes(); @@ -236,7 +227,6 @@ export async function getIcechunkDims( nodeData.dimensionNames?.length > 0 ? nodeData.dimensionNames : undefined; - console.log("Being called here for some reason?") cache.set(`${initStore}_${variable}_meta`, { ...meta, shape, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 994aa3061..1b9f6131e 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,6 +1,4 @@ export {useAxisIndices} from './useAxisIndices'; -export {useCommonUniforms} from './useCommonUniforms' -export {updateCommonUniforms} from './useCommonUniforms' export {useCoordBounds} from './useCoordBounds' export {usePaddedTextures} from './usePaddedTextures' export {useDataFetcher} from './useDataFetcher' diff --git a/src/hooks/useCommonUniforms.tsx b/src/hooks/useCommonUniforms.tsx deleted file mode 100644 index 9ee56083a..000000000 --- a/src/hooks/useCommonUniforms.tsx +++ /dev/null @@ -1,125 +0,0 @@ -// useCommonUniforms.ts -import { useGlobalStore } from '@/GlobalStates/GlobalStore' -import { usePlotStore } from '@/GlobalStates/PlotStore' -import { useColormapStore } from '@/GlobalStates/ColormapStore' -import { useEffect, useMemo } from 'react' -import * as THREE from 'three' -import { useShallow } from 'zustand/shallow' -import { useCoordBounds } from './useCoordBounds' -import { invalidate } from '@react-three/fiber' - -export function useCommonUniforms() { - const {cScale, cOffset, animProg, nanTransparency, nanColor, fillValue, maskTexture, maskValue, valueRange, - useBorderTexture, borderColor, borderWidth, borderTexture, is360Deg, showBorders} = usePlotStore(useShallow(s=>({ - cScale: s.cScale, cOffset: s.cOffset, animProg: s.animProg, nanTransparency: s.nanTransparency, nanColor: s.nanColor, - fillValue: s.fillValue, maskTexture: s.maskTexture, maskValue: s.maskValue, valueRange: s.valueRange, - useBorderTexture: s.useBorderTexture, borderColor: s.borderColor, borderWidth: s.borderWidth, borderTexture: s.borderTexture, - is360Deg: s.is360Deg, showBorders: s.showBorders - }))) - const { textureArrayDepths, remapBorders, remapTexture, valueScales, useF16Textures, bivariate} = useGlobalStore(useShallow(s => ({ - textureArrayDepths: s.textureArrayDepths, remapBorders: s.remapBorders, remapTexture: s.remapTexture, - valueScales: s.valueScales, useF16Textures: s.useF16Textures, bivariate: s.bivariate - }))) - const {lonBounds, latBounds} = useCoordBounds() - const {colormap, bottomLeft, bottomRight, topLeft, resolution, mixMode, bivariateSelection} = useColormapStore(useShallow(s => ({ - colormap: s.colormap, bottomLeft: s.bottomLeft, bottomRight: s.bottomRight, - topLeft: s.topLeft, resolution: s.resolution, mixMode: s.mixMode, bivariateSelection: s.bivariateSelection - }))) - const uniforms = useMemo(() => ({ - cScale: {value: cScale}, - cOffset: {value: cOffset}, - maskTexture: {value: maskTexture}, - maskValue: {value: maskValue}, - useBorderTexture: {value: useBorderTexture && showBorders}, - borderTexture: {value: borderTexture}, - borderWidth: {value: borderWidth}, - borderColor: {value: new THREE.Color(borderColor).convertLinearToSRGB()}, - remapBorders : {value: Boolean(remapBorders) && !Boolean(remapTexture)}, - is360: {value: is360Deg}, - useF16: {value: useF16Textures}, - threshold: {value: new THREE.Vector2(valueRange[0],valueRange[1])}, - latBounds: {value: new THREE.Vector2(latBounds[0], latBounds[1])}, - lonBounds: {value: new THREE.Vector2(lonBounds[0], lonBounds[1])}, - textureDepths: {value: new THREE.Vector3(textureArrayDepths[2], textureArrayDepths[1], textureArrayDepths[0])}, - cmap : { value : colormap}, - animateProg: {value: animProg}, - nanColor: {value : new THREE.Color(nanColor).convertLinearToSRGB()}, - nanAlpha: {value: 1 - nanTransparency}, - fillValue: {value: fillValue?? NaN}, - valueRange: {value: new THREE.Vector2(valueScales[bivariateSelection].minVal, valueScales[bivariateSelection].maxVal)}, - bottomLeft: {value: new THREE.Color(bottomLeft).convertLinearToSRGB()}, - bottomRight: {value: new THREE.Color(bottomRight).convertLinearToSRGB()}, - topLeft: {value: new THREE.Color(topLeft).convertLinearToSRGB()}, - resolution: {value: resolution}, - bivariate: {value: bivariate}, - mixMode: {value: mixMode}, - bivariateSelection: {value: bivariateSelection} - }), [ - cScale, cOffset, animProg, nanTransparency, nanColor, fillValue, maskTexture, maskValue, valueRange, - textureArrayDepths, colormap, lonBounds, latBounds, useBorderTexture, borderTexture, borderWidth, - borderColor, remapBorders, remapTexture, is360Deg, showBorders, valueScales, useF16Textures, - bottomLeft, bottomRight, topLeft, resolution, bivariate, mixMode, bivariateSelection - ]) - return uniforms -} - -export function updateCommonUniforms(material: THREE.ShaderMaterial){ - const {cScale, cOffset, animProg, nanTransparency, nanColor, fillValue, maskTexture, maskValue, valueRange, - useBorderTexture, borderColor, borderWidth, is360Deg, showBorders} = usePlotStore(useShallow(s=>({ - cScale: s.cScale, cOffset: s.cOffset, animProg: s.animProg, nanTransparency: s.nanTransparency, nanColor: s.nanColor, - fillValue: s.fillValue, maskTexture: s.maskTexture, maskValue: s.maskValue, valueRange: s.valueRange, - useBorderTexture: s.useBorderTexture, borderColor: s.borderColor, borderWidth: s.borderWidth, - is360Deg: s.is360Deg, showBorders: s.showBorders - }))) - const { valueScales, useF16Textures, bivariate} = useGlobalStore(useShallow(s => ({ - valueScales: s.valueScales, useF16Textures: s.useF16Textures, bivariate: s.bivariate - }))) - const {lonBounds, latBounds} = useCoordBounds() - const {colormap, bottomLeft, bottomRight, topLeft, resolution, mixMode, bivariateSelection} = useColormapStore(useShallow(s => ({ - colormap: s.colormap, bottomLeft: s.bottomLeft, bottomRight: s.bottomRight, - topLeft: s.topLeft, resolution: s.resolution, mixMode: s.mixMode, bivariateSelection: s.bivariateSelection - }))) - useEffect(()=>{ - // Cleanup function to dispose materials when they are remade in parent component - if (material){ - material.dispose() - } - },[material]) - - useEffect(()=>{ - if (!material) return; - const uniforms = material.uniforms; - uniforms.cOffset.value = cOffset; - uniforms.cmap. value = colormap; - uniforms.animateProg.value = animProg; - uniforms.nanColor.value = new THREE.Color(nanColor).convertLinearToSRGB(); - uniforms.nanAlpha.value = 1 - nanTransparency; - uniforms.cScale.value = cScale; - uniforms.threshold.value.set(valueRange[0], valueRange[1]); - uniforms.latBounds.value = new THREE.Vector2(latBounds[0], latBounds[1]); - uniforms.lonBounds.value = new THREE.Vector2(lonBounds[0], lonBounds[1]); - uniforms.maskValue.value = maskValue; - uniforms.fillValue.value = fillValue?? NaN; - uniforms.useBorderTexture.value = useBorderTexture && showBorders; - uniforms.borderColor.value = new THREE.Color(borderColor).convertLinearToSRGB(); - uniforms.borderWidth.value = borderWidth; - uniforms.is360.value = is360Deg; - uniforms.valueRange.value = new THREE.Vector2(valueScales[bivariateSelection].minVal, valueScales[bivariateSelection].maxVal); - uniforms.useF16.value = useF16Textures; - uniforms.bottomLeft.value = new THREE.Color(bottomLeft).convertLinearToSRGB(); - uniforms.bottomRight.value = new THREE.Color(bottomRight).convertLinearToSRGB(); - uniforms.topLeft.value = new THREE.Color(topLeft).convertLinearToSRGB(); - uniforms.resolution.value = resolution; - uniforms.mixMode.value = mixMode; - uniforms.bivariateSelection.value = bivariateSelection; - uniforms.bivariate.value = bivariate; - invalidate(); - },[ - cScale, cOffset, animProg, nanTransparency, nanColor, fillValue, maskTexture, maskValue, valueRange, - colormap, lonBounds, latBounds, useBorderTexture, borderColor, borderWidth, is360Deg, showBorders, - valueScales, useF16Textures, bottomLeft, bottomRight, topLeft, resolution, mixMode, bivariateSelection, - bivariate - ]) - - return; -} \ No newline at end of file diff --git a/src/hooks/useDataFetcher.tsx b/src/hooks/useDataFetcher.tsx index 91c3e8ccb..2feabde2f 100644 --- a/src/hooks/useDataFetcher.tsx +++ b/src/hooks/useDataFetcher.tsx @@ -11,6 +11,7 @@ import { handleIrregularGrid, reproject } from '@/components/textures/Projection import { parseExtent } from '@/utils/parseExtent'; import { createDataTexture } from '@/components/textures/TextureMakers'; import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; +import { remapTexture } from '@/components/textures/TSL/utils/uniforms'; export const useDataFetcher = () => { const { variable, bivariate, variable2, mainTextures, setUnits, setIsFlat, setUseF16Textures, @@ -123,22 +124,30 @@ export const useDataFetcher = () => { }, [reFetch]); // ---- InterpPixels ---- // - useEffect(()=> { - if (!mainTextures) return; - const updated = mainTextures.map(tex => { - const clone = tex.clone(); - if (interpPixels) { - clone.minFilter = THREE.LinearFilter; - clone.magFilter = THREE.LinearFilter; - } else { - clone.minFilter = THREE.NearestFilter; - clone.magFilter = THREE.NearestFilter; - } - clone.needsUpdate = true; - return clone ; - }); - setMainTextures(updated as THREE.Data3DTexture[] | THREE.DataTexture[]); - },[interpPixels]) +// useEffect(()=> { +// if (!mainTextures) return; +// const updated = mainTextures.map(tex => { +// const clone = tex.clone(); +// if (interpPixels) { +// clone.minFilter = THREE.LinearFilter; +// clone.magFilter = THREE.LinearFilter; +// } else { +// clone.minFilter = THREE.NearestFilter; +// clone.magFilter = THREE.NearestFilter; +// } +// clone.needsUpdate = true; +// return clone ; +// }); +// if (interpPixels){ +// remapTexture.value.minFilter = THREE.LinearFilter +// remapTexture.value.magFilter = THREE.LinearFilter +// } else { +// remapTexture.value.minFilter = THREE.NearestFilter +// remapTexture.value.magFilter = THREE.NearestFilter +// } +// remapTexture.value.needsUpdate = true; +// setMainTextures(updated as THREE.Data3DTexture[] | THREE.DataTexture[]); +// },[interpPixels]) useEffect(() => { // This cleanup function will run when the `textures` state is about to change,