From 2cfb61c5ee1056906812b0a4145162ed3fa0738e Mon Sep 17 00:00:00 2001 From: Lazaro Alonso Date: Wed, 22 Jul 2026 18:24:19 +0200 Subject: [PATCH 01/18] one-shot flash --- src/GlobalStates/PlotStore.ts | 20 +++++ src/components/computation/shaders/frag.glsl | 47 ++++++++++-- src/components/plots/DataCube.tsx | 36 ++++++++- src/components/plots/FlatBlocks.tsx | 28 ++++++- src/components/plots/FlatMap.tsx | 37 ++++++++-- src/components/plots/PointCloud.tsx | 36 ++++++++- src/components/plots/Sphere.tsx | 32 +++++++- src/components/plots/SphereBlocks.tsx | 33 ++++++++- src/components/textures/colormap.tsx | 34 +++++++++ src/components/textures/index.ts | 5 +- src/components/textures/shaders/flatFrag.glsl | 46 ++++++++++-- .../textures/shaders/fragmentOpt.glsl | 49 ++++++++++--- .../textures/shaders/pointFrag.glsl | 46 +++++++++++- .../textures/shaders/sphereBlocksFrag.glsl | 48 ++++++++++-- .../textures/shaders/sphereFrag.glsl | 45 +++++++++--- .../textures/shaders/volFragment.glsl | 71 ++++++++++++------ src/components/ui/Colorbar.tsx | 61 ++++++++++++---- src/components/ui/MainPanel/Colormaps.tsx | 73 ++++++++++++++++++- 18 files changed, 649 insertions(+), 98 deletions(-) diff --git a/src/GlobalStates/PlotStore.ts b/src/GlobalStates/PlotStore.ts index def20607..4931ddd8 100644 --- a/src/GlobalStates/PlotStore.ts +++ b/src/GlobalStates/PlotStore.ts @@ -67,6 +67,11 @@ type PlotState ={ camera: THREE.Camera | undefined; nativeCRS: string | undefined; destCRS: string | undefined; + colorScale: string; + lowclip: string; + highclip: string; + useLowclip: boolean; + useHighclip: boolean; setQuality: (quality: number) => void; setTimeScale: (timeScale : number) =>void; @@ -91,6 +96,11 @@ type PlotState ={ setAnimProg: (animProg: number) => void; setCOffset: (cOffset: number) => void; setCScale: (cScale: number) => void; + setColorScale: (colorScale: string) => void; + setLowclip: (lowclip: string) => void; + setHighclip: (highclip: string) => void; + setUseLowclip: (useLowclip: boolean) => void; + setUseHighclip: (useHighclip: boolean) => void; setUseFragOpt: (useFragOpt: boolean) => void; setResetCamera: (resetCamera: boolean) => void; setUseCustomColor: (useCustomColor: boolean) => void; @@ -193,6 +203,11 @@ export const usePlotStore = create((set, get) => ({ camera: undefined, nativeCRS: undefined, destCRS: undefined, + colorScale: "identity", + lowclip: "#000000", + highclip: "#ffffff", + useLowclip: false, + useHighclip: false, setVTransferRange: (vTransferRange) => set({ vTransferRange }), setVTransferScale: (vTransferScale) => set({ vTransferScale }), @@ -219,6 +234,11 @@ export const usePlotStore = create((set, get) => ({ setAnimProg: (animProg) => set({ animProg }), setCOffset: (cOffset) => set({ cOffset }), setCScale: (cScale) => set({ cScale }), + setColorScale: (colorScale) => set({ colorScale }), + setLowclip: (lowclip) => set({ lowclip }), + setHighclip: (highclip) => set({ highclip }), + setUseLowclip: (useLowclip) => set({ useLowclip }), + setUseHighclip: (useHighclip) => set({ useHighclip }), setUseFragOpt: (useFragOpt) => set({ useFragOpt }), setResetCamera: (resetCamera) => set({ resetCamera }), setUseCustomColor: (useCustomColor) => set({ useCustomColor }), diff --git a/src/components/computation/shaders/frag.glsl b/src/components/computation/shaders/frag.glsl index 527b1fd9..11f7d8e1 100644 --- a/src/components/computation/shaders/frag.glsl +++ b/src/components/computation/shaders/frag.glsl @@ -3,17 +3,54 @@ uniform sampler2D cmap; uniform float cOffset; uniform float cScale; +uniform vec2 threshold; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; in vec2 vUv; out vec4 Color; +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} + void main() { - vec4 val = texture(data,vUv); + vec4 val = texture(data, vUv); float d = val.x; - float sampLoc = d == 1. ? d : (d - 0.5)*cScale + 0.5; - sampLoc = d == 1. ? d : min(sampLoc+cOffset,0.99); - vec4 color = texture(cmap, vec2(sampLoc,0.5)); - color.a = val.x > 0.999 ? 0. : 1.; + if (d >= 0.999) { + Color = vec4(0.0); + return; + } + if (threshold.y > threshold.x) { + if (d < threshold.x) { + if (useLowclip) { Color = lowclip; return; } + else { Color = vec4(0.0); return; } + } + if (d > threshold.y) { + if (useHighclip) { Color = highclip; return; } + else { Color = vec4(0.0); return; } + } + } + + float normD = (threshold.y > threshold.x) ? clamp((d - threshold.x) / (threshold.y - threshold.x), 0.0, 1.0) : d; + float scaledD = applyColorScale(normD, colorScale); + float sampLoc = min(scaledD * cScale + cOffset, 0.99); + vec4 color = texture(cmap, vec2(sampLoc, 0.5)); + color.a = 1.0; Color = color; } \ No newline at end of file diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 0a787b8b..05cfd361 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -3,6 +3,7 @@ import * as THREE from 'three' import { vertexShader, fragmentShader, fragOpt, orthoVertex } from '@/components/textures/shaders'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; +import { colorScaleToId } from '@/components/textures'; import { useShallow } from 'zustand/shallow'; import { invalidate, useFrame } from '@react-three/fiber'; import { deg2rad } from '@/utils/HelperFuncs'; @@ -11,6 +12,17 @@ import { UVCube } from '@/components/plots' import { ColumnMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); +} + interface DataCubeProps { volTexture: THREE.Data3DTexture[] | THREE.DataTexture[] | null, } @@ -27,7 +39,8 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { const { valueRange, xRange, yRange, zRange, quality, useOrtho, animProg, cScale, cOffset, useFragOpt, transparency, maskTexture, maskValue, - nanTransparency, nanColor, vTransferRange, vTransferScale, fillValue} = usePlotStore(useShallow(state => ({ + nanTransparency, nanColor, vTransferRange, vTransferScale, fillValue, + colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ valueRange: state.valueRange, xRange: state.xRange, yRange: state.yRange, zRange: state.zRange, quality: state.quality, useOrtho: state.useOrtho, @@ -41,6 +54,11 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { vTransferRange: state.vTransferRange, vTransferScale: state.vTransferScale, fillValue: state.fillValue, + colorScale: state.colorScale, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, }))) const meshRef = useRef(null!); const aspectRatio = shape.y/shape.x @@ -71,7 +89,12 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { useClipScale: {value: vTransferRange}, nanAlpha: {value: 1-nanTransparency}, nanColor: {value: new THREE.Color(nanColor)}, - fillValue: {value: fillValue?? NaN} + fillValue: {value: fillValue?? NaN}, + colorScale: {value: colorScaleToId(colorScale)}, + lowclip: {value: parseColorToVec4(lowclip)}, + highclip: {value: parseColorToVec4(highclip)}, + useLowclip: {value: useLowclip}, + useHighclip: {value: useHighclip}, }, defines: { USE_VORIGIN: 1, @@ -107,10 +130,15 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { uniforms.opacityMag.value = vTransferScale; uniforms.useClipScale.value = vTransferRange; uniforms.fillValue.value = fillValue?? NaN; - uniforms.maskValue.value = maskValue + uniforms.maskValue.value = maskValue; + uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.lowclip.value = parseColorToVec4(lowclip); + uniforms.highclip.value = parseColorToVec4(highclip); + uniforms.useLowclip.value = useLowclip; + uniforms.useHighclip.value = useHighclip; invalidate() // Needed because Won't trigger re-render if camera is stationary. } - }, [shape, colormap, cOffset, cScale, valueRange, xRange, yRange, zRange, aspectRatio, latBounds, lonBounds, quality, animProg, transparency, nanTransparency, nanColor, maskValue, fillValue, vTransferScale, vTransferRange]); + }, [shape, colormap, cOffset, cScale, valueRange, xRange, yRange, zRange, aspectRatio, latBounds, lonBounds, quality, animProg, transparency, nanTransparency, nanColor, maskValue, fillValue, vTransferScale, vTransferRange, colorScale, lowclip, highclip, useLowclip, useHighclip]); 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); diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index f527a86d..457f4d4a 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -11,6 +11,18 @@ import { deg2rad } from '@/utils/HelperFuncs' import { useCoordBounds } from '@/hooks/useCoordBounds' import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; +import { colorScaleToId } from '@/components/textures'; + +function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); +} const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); @@ -26,12 +38,14 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] remapTexture: state.remapTexture }))) const { animProg, cOffset, cScale, nanColor, nanTransparency, displacement, fillValue, valueRange, offsetNegatives, rotateFlat, maskTexture, maskValue, - } = usePlotStore(useShallow(state=> ({ + colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ animate: state.animate, animProg: state.animProg, cOffset: state.cOffset, cScale: state.cScale, nanColor: state.nanColor, nanTransparency: state.nanTransparency, displacement: state.displacement, valueRange:state.valueRange, sphereResolution: state.sphereResolution, offsetNegatives: state.offsetNegatives, rotateFlat:state.rotateFlat, maskTexture:state.maskTexture, maskValue:state.maskValue, fillValue:state.fillValue, + colorScale: state.colorScale, lowclip: state.lowclip, highclip: state.highclip, + useLowclip: state.useLowclip, useHighclip: state.useHighclip, }))) const {analysisMode, axis} = useAnalysisStore(useShallow(state => ({ analysisMode: state.analysisMode, axis:state.axis @@ -103,6 +117,11 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] displaceZero: {value: offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) }, displacement: {value: displacement}, fillValue: {value: fillValue?? NaN}, + colorScale: {value: colorScaleToId(colorScale)}, + lowclip: {value: parseColorToVec4(lowclip)}, + highclip: {value: parseColorToVec4(highclip)}, + useLowclip: {value: useLowclip}, + useHighclip: {value: useHighclip}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), @@ -134,9 +153,14 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] uniforms.aspect.value = width/height; uniforms.maskValue.value = maskValue; uniforms.fillValue.value = fillValue?? NaN; + uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.lowclip.value = parseColorToVec4(lowclip); + uniforms.highclip.value = parseColorToVec4(highclip); + uniforms.useLowclip.value = useLowclip; + uniforms.useHighclip.value = useHighclip; } invalidate(); - },[animProg, valueScales, displacement, colormap, cScale, cOffset, offsetNegatives, valueRange, textures, fillValue, analysisMode, axis, width, height, latBounds, lonBounds, maskValue]) + },[animProg, valueScales, displacement, colormap, cScale, cOffset, offsetNegatives, valueRange, textures, fillValue, analysisMode, axis, width, height, latBounds, lonBounds, maskValue, colorScale, lowclip, highclip, useLowclip, useHighclip]) return ( diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index de2ff35c..30baa62e 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -11,12 +11,24 @@ import { useShallow } from 'zustand/shallow' import { ThreeEvent } from '@react-three/fiber'; import { coarsenFlatArray, GetCurrentArray, GetTimeSeries, parseUVCoords, deg2rad } from '@/utils/HelperFuncs'; import { sampleCRS } from '../textures/ProjectionTexture'; -import { evaluateColorMap } from '@/components/textures'; +import { evaluateColorMap, colorScaleToId } from '@/components/textures'; import { useCoordBounds } from '@/hooks/useCoordBounds'; import { flatFrag } from '../textures/shaders'; import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; + +function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); +} + interface InfoSettersProps{ setLoc: React.Dispatch>; setShowInfo: React.Dispatch>; @@ -43,7 +55,7 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT const {cScale, cOffset, animProg, nanTransparency, nanColor, zSlice, ySlice, xSlice, selectTS, fillValue, coarsen, maskTexture, maskValue, valueRange, - getColorIdx, incrementColorIdx} = usePlotStore(useShallow(state => ({ + getColorIdx, incrementColorIdx, colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ cOffset: state.cOffset, cScale: state.cScale, resetAnim: state.resetAnim, animate: state.animate, animProg: state.animProg, nanTransparency: state.nanTransparency, @@ -52,7 +64,12 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT selectTS: state.selectTS, coarsen: state.coarsen, maskTexture:state.maskTexture, maskValue:state.maskValue, fillValue: state.fillValue, getColorIdx: state.getColorIdx, - incrementColorIdx: state.incrementColorIdx + incrementColorIdx: state.incrementColorIdx, + colorScale: state.colorScale, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, }))) const {axis, analysisMode, analysisArray} = useAnalysisStore(useShallow(state=> ({ axis: state.axis, @@ -218,6 +235,11 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT nanColor: {value : new THREE.Color(nanColor)}, nanAlpha: {value: 1 - nanTransparency}, fillValue: {value: fillValue?? NaN}, + colorScale: {value: colorScaleToId(colorScale)}, + lowclip: {value: parseColorToVec4(lowclip)}, + highclip: {value: parseColorToVec4(highclip)}, + useLowclip: {value: useLowclip}, + useHighclip: {value: useHighclip}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), @@ -241,9 +263,14 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT uniforms.latBounds.value = new THREE.Vector2(deg2rad(latBounds[0]), deg2rad(latBounds[1])) uniforms.lonBounds.value = new THREE.Vector2(deg2rad(lonBounds[0]), deg2rad(lonBounds[1])) uniforms.maskValue.value = maskValue; - uniforms.fillValue.value = fillValue?? NaN + uniforms.fillValue.value = fillValue?? NaN; + uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.lowclip.value = parseColorToVec4(lowclip); + uniforms.highclip.value = parseColorToVec4(highclip); + uniforms.useLowclip.value = useLowclip; + uniforms.useHighclip.value = useHighclip; } - },[cScale, cOffset, colormap, animProg, nanColor, nanTransparency, latBounds, lonBounds, fillValue, maskValue, valueRange]) + },[cScale, cOffset, colormap, animProg, nanColor, nanTransparency, latBounds, lonBounds, fillValue, maskValue, valueRange, colorScale, lowclip, highclip, useLowclip, useHighclip]) useEffect(()=>{ // This is duplicated. Probably shoud just move it to Plot.tsx useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) diff --git a/src/components/plots/PointCloud.tsx b/src/components/plots/PointCloud.tsx index a2cbbb64..0d2adce5 100644 --- a/src/components/plots/PointCloud.tsx +++ b/src/components/plots/PointCloud.tsx @@ -10,6 +10,18 @@ import { UVCube } from './UVCube'; import { ColumnMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { colorScaleToId } from '@/components/textures'; + +function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); +} interface PCProps { texture: THREE.Data3DTexture[] | null, @@ -52,7 +64,8 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ }))) const {scalePoints, scaleIntensity, pointSize, cScale, cOffset, valueRange, animProg, timeScale, xRange, yRange, zRange, fillValue, - maskTexture, maskValue, disablePointScale} = usePlotStore(useShallow(state => ({ + maskTexture, maskValue, disablePointScale, + colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ scalePoints: state.scalePoints, scaleIntensity: state.scaleIntensity, pointSize: state.pointSize, @@ -67,7 +80,12 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ fillValue:state.fillValue, maskTexture: state.maskTexture, maskValue: state.maskValue, - disablePointScale: state.disablePointScale + disablePointScale: state.disablePointScale, + colorScale: state.colorScale, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, }))) //Extract data and shape from Data3DTexture @@ -132,7 +150,12 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ 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])}, - fillValue: {value: fillValue?? NaN} + fillValue: {value: fillValue?? NaN}, + colorScale: {value: colorScaleToId(colorScale)}, + lowclip: {value: parseColorToVec4(lowclip)}, + highclip: {value: parseColorToVec4(highclip)}, + useLowclip: {value: useLowclip}, + useHighclip: {value: useHighclip}, }, defines: { GLOBAL_SCALE: globalscale*2, @@ -172,8 +195,13 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ uniforms.fillValue.value = fillValue?? NaN uniforms.maskValue.value = maskValue uniforms.aspect.value = shape.x/shape.y; + uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.lowclip.value = parseColorToVec4(lowclip); + uniforms.highclip.value = parseColorToVec4(highclip); + uniforms.useLowclip.value = useLowclip; + uniforms.useHighclip.value = useHighclip; } - }, [volTexture, depthRatio, depth, height, shape, width, pointSize, colormap, cOffset, cScale, valueRange, scalePoints, scaleIntensity, animProg, xRange, yRange, fillValue, zRange, maskValue]); + }, [volTexture, depthRatio, depth, height, shape, width, pointSize, colormap, cOffset, cScale, valueRange, scalePoints, scaleIntensity, animProg, xRange, yRange, fillValue, zRange, maskValue, colorScale, lowclip, highclip, useLowclip, useHighclip]); return ( diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index 12c10a91..0effdf65 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -5,12 +5,23 @@ import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow' import { parseUVCoords, GetTimeSeries, GetCurrentArray, deg2rad } from '@/utils/HelperFuncs'; -import { evaluateColorMap } from '@/components/textures'; +import { evaluateColorMap, colorScaleToId } from '@/components/textures'; import { useCoordBounds } from '@/hooks/useCoordBounds' import { SquareMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { useAxisIndices } from '@/hooks'; import { sphereVertex, sphereFrag } from '@/components/textures/shaders' + +function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); +} function XYZtoRemap(xyz : THREE.Vector3, latBounds: number[], lonBounds : number[]){ const lon = Math.atan2(xyz.z,xyz.x) const lat = Math.asin(xyz.y); @@ -64,7 +75,12 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture fillValue:state.fillValue, maskValue:state.maskValue, borderTexture:state.borderTexture, maskTexture:state.maskTexture, getColorIdx: state.getColorIdx, - incrementColorIdx: state.incrementColorIdx + incrementColorIdx: state.incrementColorIdx, + colorScale: state.colorScale, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, }))) const {xIdx, yIdx, zIdx} = useAxisIndices() @@ -102,6 +118,11 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture displaceZero: {value: -valueScales.minVal/(valueScales.maxVal-valueScales.minVal)}, displacement: {value: sphereDisplacement}, fillValue: {value: NaN}, + colorScale: {value: colorScaleToId(colorScale)}, + lowclip: {value: parseColorToVec4(lowclip)}, + highclip: {value: parseColorToVec4(highclip)}, + useLowclip: {value: useLowclip}, + useHighclip: {value: useHighclip}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), @@ -146,6 +167,11 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture uniforms.displaceZero.value = -valueScales.minVal/(valueScales.maxVal-valueScales.minVal) uniforms.displacement.value = sphereDisplacement uniforms.fillValue.value = fillValue?? NaN + uniforms.colorScale.value = colorScaleToId(colorScale) + uniforms.lowclip.value = parseColorToVec4(lowclip) + uniforms.highclip.value = parseColorToVec4(highclip) + uniforms.useLowclip.value = useLowclip + uniforms.useHighclip.value = useHighclip } useEffect(()=>{ @@ -155,7 +181,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture if (backMaterial){ updateMaterial(backMaterial) } - },[textures, remapTexture, animProg, colormap, cOffset, cScale, animate, lonBounds, latBounds, nanColor, nanTransparency, sphereDisplacement,valueRange, fillValue, maskValue, valueScales]) + },[textures, remapTexture, animProg, colormap, cOffset, cScale, animate, lonBounds, latBounds, nanColor, nanTransparency, sphereDisplacement,valueRange, fillValue, maskValue, valueScales, colorScale, lowclip, highclip, useLowclip, useHighclip]) function HandleTimeSeries(event: THREE.Intersection){ diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx index 23ed63da..e6be1859 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -9,6 +9,18 @@ import { invalidate } from '@react-three/fiber' import { deg2rad } from '@/utils/HelperFuncs' import { useCoordBounds } from '@/hooks/useCoordBounds' import { usePaddedTextures } from '@/hooks/usePaddedTextures'; +import { colorScaleToId } from '@/components/textures'; + +function parseColorToVec4(hex: string, alpha = 1.0): THREE.Vector4 { + if (!hex) return new THREE.Vector4(0, 0, 0, alpha); + const cleanHex = hex.replace('#', ''); + const bigint = parseInt(cleanHex, 16); + if (isNaN(bigint)) return new THREE.Vector4(0, 0, 0, alpha); + const r = ((bigint >> 16) & 255) / 255; + const g = ((bigint >> 8) & 255) / 255; + const b = (bigint & 255) / 255; + return new THREE.Vector4(r, g, b, alpha); +} const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] | THREE.DataTexture[] | null}) => { const textures = usePaddedTextures(propTextures); const {colormap, isFlat, valueScales, @@ -21,7 +33,8 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ flipY: state.flipY, remapTexture: state.remapTexture }))) - const { animProg, cOffset, cScale, nanColor, nanTransparency, sphereDisplacement, offsetNegatives, fillValue, valueRange, maskTexture, maskValue} = usePlotStore(useShallow(state=> ({ + const { animProg, cOffset, cScale, nanColor, nanTransparency, sphereDisplacement, offsetNegatives, fillValue, valueRange, maskTexture, maskValue, + colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ animate: state.animate, animProg: state.animProg, cOffset: state.cOffset, @@ -35,6 +48,11 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ valueRange: state.valueRange, maskTexture: state.maskTexture, maskValue: state.maskValue, + colorScale: state.colorScale, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, }))) const count = useMemo(()=>{ @@ -75,7 +93,6 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ ); return geo },[dataShape]) - const {lonBounds, latBounds} = useCoordBounds() const shaderMaterial = useMemo(()=>{ @@ -99,6 +116,11 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ displaceZero: {value: offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal))}, displacement: {value: sphereDisplacement}, fillValue: {value: fillValue?? NaN}, + colorScale: {value: colorScaleToId(colorScale)}, + lowclip: {value: parseColorToVec4(lowclip)}, + highclip: {value: parseColorToVec4(highclip)}, + useLowclip: {value: useLowclip}, + useHighclip: {value: useHighclip}, }, defines:{ ...(isFlat ? { IS_FLAT: true } : {}), @@ -136,9 +158,14 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ uniforms.displaceZero.value = offsetNegatives ? 0 : (-valueScales.minVal/(valueScales.maxVal-valueScales.minVal)) uniforms.fillValue.value = fillValue?? NaN uniforms.maskValue.value = maskValue + uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.lowclip.value = parseColorToVec4(lowclip); + uniforms.highclip.value = parseColorToVec4(highclip); + uniforms.useLowclip.value = useLowclip; + uniforms.useHighclip.value = useHighclip; } invalidate(); - },[animProg, valueScales, sphereDisplacement, colormap, cScale, cOffset, latBounds, lonBounds, valueRange, offsetNegatives, textures, remapTexture, maskValue, fillValue]) + },[animProg, valueScales, sphereDisplacement, colormap, cScale, cOffset, latBounds, lonBounds, valueRange, offsetNegatives, textures, remapTexture, maskValue, fillValue, colorScale, lowclip, highclip, useLowclip, useHighclip]) const nanMaterial = useMemo(()=>new THREE.MeshBasicMaterial({color:nanColor}),[]) nanMaterial.transparent = true; diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 4e42e9c9..79d73833 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -3,6 +3,40 @@ import { colorschemes, get, findColorScheme } from 'color-schemes-js'; 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']; +export const COLOR_SCALE_OPTIONS = [ + { label: 'x (Linear)', value: 'identity' }, + { label: 'log(x)', value: 'log(x)' }, + { label: 'log(1+x)', value: 'log(1+x)' }, + { label: 'sign(x)*sqrt(abs(x))', value: 'sign(x)*sqrt(abs(x))' }, + { label: 'exp(x)/100', value: 'exp(x)/100' }, +] as const; + +export function colorScaleToId(colorScale: string): number { + switch (colorScale) { + case 'log(x)': return 1; + case 'log(1+x)': return 2; + case 'sign(x)*sqrt(abs(x))': return 3; + case 'exp(x)/100': return 4; + case 'identity': + default: return 0; + } +} + +export function applyColorScale(x: number, scaleType: string): number { + if (scaleType === 'log(x)') { + const eps = 0.000001; + const clamped = Math.max(x, eps); + return (Math.log(clamped) - Math.log(eps)) / (Math.log(1.0 + eps) - Math.log(eps)); + } else if (scaleType === 'log(1+x)') { + return Math.log(1.0 + Math.max(x, 0.0)) / Math.log(2.0); + } else if (scaleType === 'sign(x)*sqrt(abs(x))') { + return Math.sign(x) * Math.sqrt(Math.abs(x)); + } else if (scaleType === 'exp(x)/100') { + return (Math.exp(x) - 1.0) / (Math.exp(1.0) - 1.0); + } + return x; +} + export const availableColorMapNames = Object.keys(colorschemes).sort(); export type ColormapEntry = { diff --git a/src/components/textures/index.ts b/src/components/textures/index.ts index 435212ac..d5f3bbfb 100644 --- a/src/components/textures/index.ts +++ b/src/components/textures/index.ts @@ -1,4 +1,4 @@ -import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss, colormapIndex } from './colormap'; +import { GetColorMapTexture, colormaps, evaluateColorMap, availableColorMapNames, getColormapGradientCss, colormapIndex, COLOR_SCALE_OPTIONS, colorScaleToId, applyColorScale } from './colormap'; import {ArrayToTexture, CreateTexture}from './TextureMakers' export { @@ -8,6 +8,9 @@ export { availableColorMapNames, getColormapGradientCss, colormapIndex, + COLOR_SCALE_OPTIONS, + colorScaleToId, + applyColorScale, ArrayToTexture, CreateTexture } \ No newline at end of file diff --git a/src/components/textures/shaders/flatFrag.glsl b/src/components/textures/shaders/flatFrag.glsl index 1ac1b15e..f0fc220f 100644 --- a/src/components/textures/shaders/flatFrag.glsl +++ b/src/components/textures/shaders/flatFrag.glsl @@ -21,12 +21,32 @@ uniform vec2 lonBounds; uniform vec2 threshold; uniform int maskValue; uniform float fillValue; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; in vec2 vUv; out vec4 Color; #define epsilon 0.0001 #define PI 3.14159265 +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} + vec2 realCoords(vec2 uv){ vec2 normalizedLon = lonBounds/2./PI+0.5; vec2 normalizedLat = latBounds/PI+0.5; @@ -103,15 +123,27 @@ void main() { localCoord = fract(localCoord); float strength = sample1(localCoord, textureIdx); - bool valid = (strength >= threshold.x) && (strength <= threshold.y); - if (!valid || abs(strength - fillValue) < 0.005){ - Color = vec4(0.); + bool isNaN = (strength == 1.) || (abs(strength - fillValue) < 0.005); + if (isNaN) { + Color = vec4(nanColor, nanAlpha); + return; + } + if (strength < threshold.x) { + if (useLowclip) Color = lowclip; + else Color = vec4(0.); + return; + } + if (strength > threshold.y) { + if (useHighclip) Color = highclip; + else Color = vec4(0.); return; } - bool isNaN = strength == 1.; - float sampLoc = isNaN ? strength: (strength)*cScale; - sampLoc = isNaN ? strength : min(sampLoc+cOffset,0.995); - Color = isNaN ? vec4(nanColor, nanAlpha) : vec4(texture2D(cmap, vec2(sampLoc, 0.5)).rgb, 1.); + + float range = max(threshold.y - threshold.x, 0.0001); + float normS = clamp((strength - threshold.x) / range, 0.0, 1.0); + float scaledS = applyColorScale(normS, colorScale); + float sampLoc = min(scaledS * cScale + cOffset, 0.995); + Color = vec4(texture(cmap, vec2(sampLoc, 0.5)).rgb, 1.0); // float check = float(texture(remapTexture,texCoord.xy).g >= 0.); // Color = vec4(check, 0., 0. , 1.); // Color = vec4(texture(remapTexture,texCoord.xy).rg, 0. , 1.); diff --git a/src/components/textures/shaders/fragmentOpt.glsl b/src/components/textures/shaders/fragmentOpt.glsl index da95a2a0..7889773a 100644 --- a/src/components/textures/shaders/fragmentOpt.glsl +++ b/src/components/textures/shaders/fragmentOpt.glsl @@ -30,10 +30,30 @@ uniform float fillValue; uniform int maskValue; uniform vec2 latBounds; uniform vec2 lonBounds; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; #define epsilon 0.000001 #define pi 3.1415926535 +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} + vec2 hitBox(vec3 orig, vec3 dir) { vec3 box_min = vec3(-(scale * 0.5)); @@ -136,7 +156,7 @@ void main() { localCoord = fract(localCoord); float d = sample1(localCoord, textureIdx); - bool cond = (d > threshold.x) && (d < threshold.y+.01); //We skip over nans if the transparency is enabled + bool cond = (d >= threshold.x) && (d <= threshold.y + 0.01) || ((d < threshold.x && useLowclip) || (d > threshold.y + 0.01 && useHighclip)); if (cond) { // Hit something interesting - switch to fine stepping @@ -150,17 +170,28 @@ void main() { if (d == 1. || abs(d - fillValue) < 0.005){ accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; alphaAcc += pow(nanAlpha, 5.); - } - else{ - float sampLoc = d*cScale; - sampLoc = min(sampLoc+cOffset,0.99); + } else if (d < threshold.x) { + if (useLowclip) { + accumColor.rgb += (1.0 - alphaAcc) * lowclip.a * lowclip.rgb; + alphaAcc += lowclip.a * (1.0 - alphaAcc); + } + } else if (d > threshold.y) { + if (useHighclip) { + accumColor.rgb += (1.0 - alphaAcc) * highclip.a * highclip.rgb; + alphaAcc += highclip.a * (1.0 - alphaAcc); + } + } else { + float range = max(threshold.y - threshold.x, 0.0001); + float normD = clamp((d - threshold.x) / range, 0.0, 1.0); + float scaledD = applyColorScale(normD, colorScale); + float sampLoc = min(scaledD * cScale + cOffset, 0.99); vec4 col = texture(cmap, vec2(sampLoc, 0.5)); float alpha; - if (useClipScale){ - float normalizedOpacity = clamp((sampLoc - threshold.x) / (threshold.y - threshold.x), 0.0, 1.0); - alpha = pow(max(normalizedOpacity, 0.001), transparency*opacityMag); + if (useClipScale) { + float normalizedOpacity = clamp(scaledD, 0.0, 1.0); + alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); } else { - alpha = pow(max(sampLoc, 0.001), transparency*opacityMag); + alpha = pow(max(sampLoc, 0.001), transparency * opacityMag); } accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; alphaAcc += alpha * (1.0 - alphaAcc); diff --git a/src/components/textures/shaders/pointFrag.glsl b/src/components/textures/shaders/pointFrag.glsl index 75d25920..c218a057 100644 --- a/src/components/textures/shaders/pointFrag.glsl +++ b/src/components/textures/shaders/pointFrag.glsl @@ -5,10 +5,52 @@ in float vValue; uniform sampler2D cmap; uniform float cScale; uniform float cOffset; +uniform vec2 valueRange; +uniform float fillValue; +uniform vec3 nanColor; +uniform float nanAlpha; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; + +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} void main() { - float sampLoc = vValue == 1. ? vValue : (vValue - 0.5)*cScale + 0.5; - sampLoc = vValue == 1. ? vValue : min(sampLoc+cOffset,0.99); + bool isNaN = (vValue == 1.) || (abs(vValue - fillValue) < 0.005); + if (isNaN) { + Color = vec4(nanColor, nanAlpha); + return; + } + if (vValue < valueRange.x) { + if (useLowclip) Color = lowclip; + else Color = vec4(0.); + return; + } + if (vValue > valueRange.y) { + if (useHighclip) Color = highclip; + else Color = vec4(0.); + return; + } + + float range = max(valueRange.y - valueRange.x, 0.0001); + float normVal = clamp((vValue - valueRange.x) / range, 0.0, 1.0); + float scaledVal = applyColorScale(normVal, colorScale); + float sampLoc = min(scaledVal * cScale + cOffset, 0.99); vec4 color = texture(cmap, vec2(sampLoc, 0.5)); color.a = 1.; Color = color; diff --git a/src/components/textures/shaders/sphereBlocksFrag.glsl b/src/components/textures/shaders/sphereBlocksFrag.glsl index a0c9f8ac..9782090b 100644 --- a/src/components/textures/shaders/sphereBlocksFrag.glsl +++ b/src/components/textures/shaders/sphereBlocksFrag.glsl @@ -1,20 +1,58 @@ uniform sampler2D cmap; uniform float cOffset; uniform float cScale; +uniform vec2 threshold; +uniform float fillValue; +uniform vec3 nanColor; +uniform float nanAlpha; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; in float vStrength; out vec4 Color; +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} void main() { float strength = vStrength; + bool isNaN = (strength == 1.) || (abs(strength - fillValue) < 0.005); + if (isNaN) { + Color = vec4(nanColor, nanAlpha); + return; + } + if (strength < threshold.x) { + if (useLowclip) Color = lowclip; + else Color = vec4(0.); + return; + } + if (strength > threshold.y) { + if (useHighclip) Color = highclip; + else Color = vec4(0.); + return; + } - strength *= cScale; - strength = min(strength+cOffset,0.996); - - vec3 sampColor = texture(cmap, vec2(strength, 0.5)).rgb; + float range = max(threshold.y - threshold.x, 0.0001); + float normS = clamp((strength - threshold.x) / range, 0.0, 1.0); + float scaledS = applyColorScale(normS, colorScale); + float sampLoc = min(scaledS * cScale + cOffset, 0.996); + vec3 sampColor = texture(cmap, vec2(sampLoc, 0.5)).rgb; Color = vec4(sampColor, 1.0); - } \ No newline at end of file diff --git a/src/components/textures/shaders/sphereFrag.glsl b/src/components/textures/shaders/sphereFrag.glsl index 7e66f1e1..5edb83a3 100644 --- a/src/components/textures/shaders/sphereFrag.glsl +++ b/src/components/textures/shaders/sphereFrag.glsl @@ -24,10 +24,30 @@ uniform vec3 nanColor; uniform float nanAlpha; uniform float fillValue; uniform int maskValue; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; #define pi 3.141592653 #define epsilon 0.0001 +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} + vec2 giveUV(vec3 position){ vec3 n = normalize(position); float latitude = asin(n.y); @@ -113,18 +133,21 @@ void main(){ #endif localCoord = fract(localCoord); float strength = sample1(localCoord, textureIdx); - bool valid = (strength >= threshold.x) && (strength <= threshold.y); - if (!valid){ - color = vec4(0.); - return; - } - bool isNaN = strength == 1. || abs(strength - fillValue) < 0.005; - strength = isNaN ? strength : (strength)*cScale; - strength = isNaN ? strength : min(strength+cOffset,0.99); - color = isNaN ? vec4(nanColor, nanAlpha) : texture(cmap, vec2(strength, 0.5)); - if (!isNaN){ - color.a = 1.; + bool isNaN = (strength == 1.) || (abs(strength - fillValue) < 0.005); + if (isNaN) { + color = vec4(nanColor, nanAlpha); + } else if (strength < threshold.x) { + color = useLowclip ? lowclip : vec4(0.); + } else if (strength > threshold.y) { + color = useHighclip ? highclip : vec4(0.); + } else { + float range = max(threshold.y - threshold.x, 0.0001); + float normS = clamp((strength - threshold.x) / range, 0.0, 1.0); + float scaledS = applyColorScale(normS, colorScale); + float sampLoc = min(scaledS * cScale + cOffset, 0.99); + color = vec4(texture(cmap, vec2(sampLoc, 0.5)).rgb, 1.0); } + if (maskValue != 0){ vec2 maskUV = giveMaskUV(aPosition); float mask = texture(maskTexture, maskUV).r; diff --git a/src/components/textures/shaders/volFragment.glsl b/src/components/textures/shaders/volFragment.glsl index 112489d6..dbf8cad3 100644 --- a/src/components/textures/shaders/volFragment.glsl +++ b/src/components/textures/shaders/volFragment.glsl @@ -30,10 +30,30 @@ uniform float fillValue; uniform int maskValue; uniform vec2 latBounds; uniform vec2 lonBounds; +uniform int colorScale; +uniform vec4 lowclip; +uniform vec4 highclip; +uniform bool useLowclip; +uniform bool useHighclip; #define epsilon 0.000001 #define pi 3.1415926535 +float applyColorScale(float x, int scaleType) { + if (scaleType == 1) { + float eps = 0.000001; + float clamped = max(x, eps); + return (log(clamped) - log(eps)) / (log(1.0 + eps) - log(eps)); + } else if (scaleType == 2) { + return log(1.0 + max(x, 0.0)) / log(2.0); + } else if (scaleType == 3) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 4) { + return (exp(x) - 1.0) / (exp(1.0) - 1.0); + } + return x; +} + vec2 hitBox(vec3 orig, vec3 dir) { vec3 box_min = vec3(-(scale * 0.5)); vec3 box_max = vec3(scale * 0.5); @@ -135,30 +155,37 @@ void main() { localCoord = fract(localCoord); float d = sample1(localCoord, textureIdx); - bool cond = (d >= threshold.x) && (d <= threshold.y); - - if (cond) { - if (d == 1. || abs(d - fillValue) < 0.005){ - accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; - alphaAcc += pow(nanAlpha, 5.); + if (d == 1. || abs(d - fillValue) < 0.005) { + accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; + alphaAcc += pow(nanAlpha, 5.); + } else if (d < threshold.x) { + if (useLowclip) { + accumColor.rgb += (1.0 - alphaAcc) * lowclip.a * lowclip.rgb; + alphaAcc += lowclip.a * (1.0 - alphaAcc); } - else{ - float sampLoc = d*cScale; - sampLoc = min(sampLoc+cOffset,0.99); - vec4 col = texture(cmap, vec2(sampLoc, 0.5)); - float alpha; - if (useClipScale){ - float normalizedOpacity = clamp((sampLoc - threshold.x) / (threshold.y - threshold.x), 0.0, 1.0); - alpha = pow(max(normalizedOpacity, 0.001), transparency*opacityMag); - } else { - alpha = pow(max(sampLoc, 0.001), transparency*opacityMag); - } - accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; - alphaAcc += alpha * (1.0 - alphaAcc); - } - - if (alphaAcc >= 1.0) break; + } else if (d > threshold.y) { + if (useHighclip) { + accumColor.rgb += (1.0 - alphaAcc) * highclip.a * highclip.rgb; + alphaAcc += highclip.a * (1.0 - alphaAcc); + } + } else { + float range = max(threshold.y - threshold.x, 0.0001); + float normD = clamp((d - threshold.x) / range, 0.0, 1.0); + float scaledD = applyColorScale(normD, colorScale); + float sampLoc = min(scaledD * cScale + cOffset, 0.99); + vec4 col = texture(cmap, vec2(sampLoc, 0.5)); + float alpha; + if (useClipScale) { + float normalizedOpacity = clamp(scaledD, 0.0, 1.0); + alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); + } else { + alpha = pow(max(sampLoc, 0.001), transparency * opacityMag); + } + accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; + alphaAcc += alpha * (1.0 - alphaAcc); } + + if (alphaAcc >= 1.0) break; } accumColor.a = alphaAcc; // Set the final accumulated alpha color = accumColor; diff --git a/src/components/ui/Colorbar.tsx b/src/components/ui/Colorbar.tsx index 561423e3..e8d09fd8 100644 --- a/src/components/ui/Colorbar.tsx +++ b/src/components/ui/Colorbar.tsx @@ -12,6 +12,8 @@ import './css/Colorbar.css' import { linspace } from '@/utils/HelperFuncs'; import Metadata from "./MetaData"; +import { applyColorScale } from "@/components/textures"; + const operationMap = { // Reductions Mean: "Mean", @@ -54,11 +56,16 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec variable: state.variable, scalingFactor: state.scalingFactor }))) - const {cScale, cOffset, setCScale, setCOffset} = usePlotStore(useShallow(state => ({ + const {cScale, cOffset, setCScale, setCOffset, colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ cScale: state.cScale, cOffset: state.cOffset, setCScale: state.setCScale, - setCOffset: state.setCOffset + setCOffset: state.setCOffset, + colorScale: state.colorScale, + lowclip: state.lowclip, + highclip: state.highclip, + useLowclip: state.useLowclip, + useHighclip: state.useHighclip, }))) const {variable2, analysisMode, operation, kernelOperation, execute} = useAnalysisStore(useShallow(state => ({ variable2: state.variable2, @@ -101,10 +108,14 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec },[colormap]) const [locs, vals] = useMemo(()=>{ - const locs = linspace(0, 100, tickCount) - const vals = linspace(newMin, newMax, tickCount) + const locs = linspace(0, 100, tickCount); + const vals = locs.map(loc => { + const t = loc / 100; + const scaledT = applyColorScale(t, colorScale); + return newMin + (newMax - newMin) * scaledT; + }); return [locs, vals] - },[ tickCount, newMin, newMax]) + },[ tickCount, newMin, newMax, colorScale]) // Mouse move handler const handleMouseMove = (e: MouseEvent) => { @@ -169,17 +180,39 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec },[origMax, origMin, scalingFactor]) useEffect(() => { - if (canvasRef.current) { - const canvas = canvasRef.current; - const ctx = canvas.getContext("2d"); - if (ctx){ - colors.forEach((color, index) => { - ctx.fillStyle = color; - ctx.fillRect(index*2, 0, 2, 24); // Each color is 1px wide and 50px tall - }); + if (canvasRef.current && colors.length > 0) { + const canvas = canvasRef.current; + const ctx = canvas.getContext("2d"); + if (ctx) { + const width = canvas.width; + const height = canvas.height; + ctx.clearRect(0, 0, width, height); + + const clipWidth = 14; + const startX = useLowclip ? clipWidth : 0; + const endX = useHighclip ? width - clipWidth : width; + const mainWidth = endX - startX; + + if (useLowclip) { + ctx.fillStyle = lowclip; + ctx.fillRect(0, 0, clipWidth, height); + } + + for (let x = 0; x < mainWidth; x++) { + const normX = x / mainWidth; + const scaledT = applyColorScale(normX, colorScale); + const colorIndex = Math.min(Math.floor(scaledT * (colors.length - 1)), colors.length - 1); + ctx.fillStyle = colors[colorIndex] || '#000'; + ctx.fillRect(startX + x, 0, 1, height); + } + + if (useHighclip) { + ctx.fillStyle = highclip; + ctx.fillRect(endX, 0, clipWidth, height); + } } } - }, [colors]); + }, [colors, colorScale, lowclip, highclip, useLowclip, useHighclip]); const analysisString = useMemo(()=>{ if (analysisMode){ const twoVar = variable2 != "Default"; diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index 7c1de364..c620fa2a 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -1,8 +1,9 @@ "use client"; import React, { useEffect, useState, useMemo, useRef } from 'react' -import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss, colormapIndex } from '@/components/textures'; +import { GetColorMapTexture, colormaps, availableColorMapNames, getColormapGradientCss, colormapIndex, COLOR_SCALE_OPTIONS } from '@/components/textures'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; +import { usePlotStore } from '@/GlobalStates/PlotStore'; import { useShallow } from 'zustand/shallow'; import { MdOutlineSwapVert } from "react-icons/md"; import { ButtonGroup } from "@/components/ui/button-group"; @@ -47,6 +48,20 @@ const Colormaps = () => { setFlipColormap: state.setFlipColormap, })) ); + const { colorScale, setColorScale, lowclip, setLowclip, highclip, setHighclip, useLowclip, setUseLowclip, useHighclip, setUseHighclip } = usePlotStore( + useShallow((state) => ({ + colorScale: state.colorScale, + setColorScale: state.setColorScale, + lowclip: state.lowclip, + setLowclip: state.setLowclip, + highclip: state.highclip, + setHighclip: state.setHighclip, + useLowclip: state.useLowclip, + setUseLowclip: state.setUseLowclip, + useHighclip: state.useHighclip, + setUseHighclip: state.setUseHighclip, + })) + ); const [popoverSide, setPopoverSide] = useState<"left" | "top">("left"); const [prevColormapName, setPrevColormapName] = useState(colormapName || ''); @@ -278,6 +293,62 @@ const Colormaps = () => { +
+
+ Color Scale: + +
+
+ + {useLowclip && ( + setLowclip(e.target.value)} + className="w-6 h-6 p-0 border-0 rounded cursor-pointer" + /> + )} +
+
+ + {useHighclip && ( + setHighclip(e.target.value)} + className="w-6 h-6 p-0 border-0 rounded cursor-pointer" + /> + )} +
+
+ Date: Wed, 22 Jul 2026 18:52:24 +0200 Subject: [PATCH 02/18] clips --- src/GlobalStates/PlotStore.ts | 4 ++ src/components/computation/shaders/frag.glsl | 28 +++++++-- src/components/plots/DataCube.tsx | 7 ++- src/components/plots/FlatBlocks.tsx | 8 ++- src/components/plots/FlatMap.tsx | 13 +++-- src/components/plots/PointCloud.tsx | 7 ++- src/components/plots/Sphere.tsx | 7 ++- src/components/plots/SphereBlocks.tsx | 7 ++- src/components/textures/colormap.tsx | 14 ++++- src/components/textures/shaders/flatFrag.glsl | 28 +++++++-- .../textures/shaders/fragmentOpt.glsl | 57 +++++++++++-------- .../textures/shaders/pointFrag.glsl | 28 +++++++-- .../textures/shaders/sphereBlocksFrag.glsl | 29 ++++++++-- .../textures/shaders/sphereFrag.glsl | 24 ++++++-- .../textures/shaders/volFragment.glsl | 55 +++++++++++------- src/components/ui/Colorbar.tsx | 11 ++-- src/components/ui/MainPanel/Colormaps.tsx | 37 +++++++++--- 17 files changed, 266 insertions(+), 98 deletions(-) diff --git a/src/GlobalStates/PlotStore.ts b/src/GlobalStates/PlotStore.ts index 4931ddd8..16c16d57 100644 --- a/src/GlobalStates/PlotStore.ts +++ b/src/GlobalStates/PlotStore.ts @@ -68,6 +68,7 @@ type PlotState ={ nativeCRS: string | undefined; destCRS: string | undefined; colorScale: string; + logConstant: number; lowclip: string; highclip: string; useLowclip: boolean; @@ -97,6 +98,7 @@ type PlotState ={ setCOffset: (cOffset: number) => void; setCScale: (cScale: number) => void; setColorScale: (colorScale: string) => void; + setLogConstant: (logConstant: number) => void; setLowclip: (lowclip: string) => void; setHighclip: (highclip: string) => void; setUseLowclip: (useLowclip: boolean) => void; @@ -204,6 +206,7 @@ export const usePlotStore = create((set, get) => ({ nativeCRS: undefined, destCRS: undefined, colorScale: "identity", + logConstant: 1.0, lowclip: "#000000", highclip: "#ffffff", useLowclip: false, @@ -235,6 +238,7 @@ export const usePlotStore = create((set, get) => ({ setCOffset: (cOffset) => set({ cOffset }), setCScale: (cScale) => set({ cScale }), setColorScale: (colorScale) => set({ colorScale }), + setLogConstant: (logConstant) => set({ logConstant }), setLowclip: (lowclip) => set({ lowclip }), setHighclip: (highclip) => set({ highclip }), setUseLowclip: (useLowclip) => set({ useLowclip }), diff --git a/src/components/computation/shaders/frag.glsl b/src/components/computation/shaders/frag.glsl index 11f7d8e1..c66d63d8 100644 --- a/src/components/computation/shaders/frag.glsl +++ b/src/components/computation/shaders/frag.glsl @@ -5,6 +5,7 @@ uniform float cOffset; uniform float cScale; uniform vec2 threshold; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; @@ -13,7 +14,7 @@ uniform bool useHighclip; in vec2 vUv; out vec4 Color; -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -21,8 +22,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -47,8 +54,21 @@ void main() { } float normD = (threshold.y > threshold.x) ? clamp((d - threshold.x) / (threshold.y - threshold.x), 0.0, 1.0) : d; - float scaledD = applyColorScale(normD, colorScale); - float sampLoc = min(scaledD * cScale + cOffset, 0.99); + float scaledD = applyColorScale(normD, colorScale, logConstant); + float rawSampLoc = scaledD * cScale + cOffset; + + if (rawSampLoc < 0.0) { + if (useLowclip) Color = lowclip; + else Color = vec4(texture(cmap, vec2(0.0, 0.5)).rgb, 1.0); + return; + } + if (rawSampLoc > 1.0) { + if (useHighclip) Color = highclip; + else Color = vec4(texture(cmap, vec2(0.995, 0.5)).rgb, 1.0); + return; + } + + float sampLoc = clamp(rawSampLoc, 0.0, 0.995); vec4 color = texture(cmap, vec2(sampLoc, 0.5)); color.a = 1.0; diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 05cfd361..b4e56177 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -40,7 +40,7 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { valueRange, xRange, yRange, zRange, quality, useOrtho, animProg, cScale, cOffset, useFragOpt, transparency, maskTexture, maskValue, nanTransparency, nanColor, vTransferRange, vTransferScale, fillValue, - colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ valueRange: state.valueRange, xRange: state.xRange, yRange: state.yRange, zRange: state.zRange, quality: state.quality, useOrtho: state.useOrtho, @@ -55,6 +55,7 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { vTransferScale: state.vTransferScale, fillValue: state.fillValue, colorScale: state.colorScale, + logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, @@ -91,6 +92,7 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { nanColor: {value: new THREE.Color(nanColor)}, fillValue: {value: fillValue?? NaN}, colorScale: {value: colorScaleToId(colorScale)}, + logConstant: {value: logConstant}, lowclip: {value: parseColorToVec4(lowclip)}, highclip: {value: parseColorToVec4(highclip)}, useLowclip: {value: useLowclip}, @@ -132,13 +134,14 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { uniforms.fillValue.value = fillValue?? NaN; uniforms.maskValue.value = maskValue; uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.logConstant.value = logConstant; uniforms.lowclip.value = parseColorToVec4(lowclip); uniforms.highclip.value = parseColorToVec4(highclip); uniforms.useLowclip.value = useLowclip; uniforms.useHighclip.value = useHighclip; invalidate() // Needed because Won't trigger re-render if camera is stationary. } - }, [shape, colormap, cOffset, cScale, valueRange, xRange, yRange, zRange, aspectRatio, latBounds, lonBounds, quality, animProg, transparency, nanTransparency, nanColor, maskValue, fillValue, vTransferScale, vTransferRange, colorScale, lowclip, highclip, useLowclip, useHighclip]); + }, [shape, colormap, cOffset, cScale, valueRange, xRange, yRange, zRange, aspectRatio, latBounds, lonBounds, quality, animProg, transparency, nanTransparency, nanColor, maskValue, fillValue, vTransferScale, vTransferRange, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]); 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); diff --git a/src/components/plots/FlatBlocks.tsx b/src/components/plots/FlatBlocks.tsx index 457f4d4a..43bc3730 100644 --- a/src/components/plots/FlatBlocks.tsx +++ b/src/components/plots/FlatBlocks.tsx @@ -38,13 +38,13 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] remapTexture: state.remapTexture }))) const { animProg, cOffset, cScale, nanColor, nanTransparency, displacement, fillValue, valueRange, offsetNegatives, rotateFlat, maskTexture, maskValue, - colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ animate: state.animate, animProg: state.animProg, cOffset: state.cOffset, cScale: state.cScale, nanColor: state.nanColor, nanTransparency: state.nanTransparency, displacement: state.displacement, valueRange:state.valueRange, sphereResolution: state.sphereResolution, offsetNegatives: state.offsetNegatives, rotateFlat:state.rotateFlat, maskTexture:state.maskTexture, maskValue:state.maskValue, fillValue:state.fillValue, - colorScale: state.colorScale, lowclip: state.lowclip, highclip: state.highclip, + colorScale: state.colorScale, logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, useHighclip: state.useHighclip, }))) const {analysisMode, axis} = useAnalysisStore(useShallow(state => ({ @@ -118,6 +118,7 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] displacement: {value: displacement}, fillValue: {value: fillValue?? NaN}, colorScale: {value: colorScaleToId(colorScale)}, + logConstant: {value: logConstant}, lowclip: {value: parseColorToVec4(lowclip)}, highclip: {value: parseColorToVec4(highclip)}, useLowclip: {value: useLowclip}, @@ -154,13 +155,14 @@ const FlatBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[] uniforms.maskValue.value = maskValue; uniforms.fillValue.value = fillValue?? NaN; uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.logConstant.value = logConstant; uniforms.lowclip.value = parseColorToVec4(lowclip); uniforms.highclip.value = parseColorToVec4(highclip); uniforms.useLowclip.value = useLowclip; uniforms.useHighclip.value = useHighclip; } invalidate(); - },[animProg, valueScales, displacement, colormap, cScale, cOffset, offsetNegatives, valueRange, textures, fillValue, analysisMode, axis, width, height, latBounds, lonBounds, maskValue, colorScale, lowclip, highclip, useLowclip, useHighclip]) + },[animProg, valueScales, displacement, colormap, cScale, cOffset, offsetNegatives, valueRange, textures, fillValue, analysisMode, axis, width, height, latBounds, lonBounds, maskValue, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) return ( diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index 30baa62e..5876f1b8 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -55,7 +55,7 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT const {cScale, cOffset, animProg, nanTransparency, nanColor, zSlice, ySlice, xSlice, selectTS, fillValue, coarsen, maskTexture, maskValue, valueRange, - getColorIdx, incrementColorIdx, colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ + getColorIdx, incrementColorIdx, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ cOffset: state.cOffset, cScale: state.cScale, resetAnim: state.resetAnim, animate: state.animate, animProg: state.animProg, nanTransparency: state.nanTransparency, @@ -66,6 +66,7 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT getColorIdx: state.getColorIdx, incrementColorIdx: state.incrementColorIdx, colorScale: state.colorScale, + logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, @@ -124,10 +125,10 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT return slices; }, [analysisMode, dimSlices, dimArrays, zSlice, ySlice, xSlice, axis, coarsen, kernelDepth, kernelSize, xIdx, yIdx, zIdx]) - const {lonBounds, latBounds} = useCoordBounds() - useEffect(()=>{ + return () => { geometry.dispose() + } },[geometry]) // ----- MOUSE MOVE ----- // @@ -214,9 +215,9 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT } } updateDimCoords({[tsID] : dimObj}) - } // ----- SHADER MATERIAL ----- // + const {lonBounds, latBounds} = useCoordBounds() const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ glslVersion: THREE.GLSL3, uniforms:{ @@ -236,6 +237,7 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT nanAlpha: {value: 1 - nanTransparency}, fillValue: {value: fillValue?? NaN}, colorScale: {value: colorScaleToId(colorScale)}, + logConstant: {value: logConstant}, lowclip: {value: parseColorToVec4(lowclip)}, highclip: {value: parseColorToVec4(highclip)}, useLowclip: {value: useLowclip}, @@ -265,12 +267,13 @@ const FlatMap = ({textures: propTextures, infoSetters} : {textures : THREE.DataT uniforms.maskValue.value = maskValue; uniforms.fillValue.value = fillValue?? NaN; uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.logConstant.value = logConstant; uniforms.lowclip.value = parseColorToVec4(lowclip); uniforms.highclip.value = parseColorToVec4(highclip); uniforms.useLowclip.value = useLowclip; uniforms.useHighclip.value = useHighclip; } - },[cScale, cOffset, colormap, animProg, nanColor, nanTransparency, latBounds, lonBounds, fillValue, maskValue, valueRange, colorScale, lowclip, highclip, useLowclip, useHighclip]) + },[cScale, cOffset, colormap, animProg, nanColor, nanTransparency, latBounds, lonBounds, fillValue, maskValue, valueRange, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) useEffect(()=>{ // This is duplicated. Probably shoud just move it to Plot.tsx useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) diff --git a/src/components/plots/PointCloud.tsx b/src/components/plots/PointCloud.tsx index 0d2adce5..8e1c5f33 100644 --- a/src/components/plots/PointCloud.tsx +++ b/src/components/plots/PointCloud.tsx @@ -65,7 +65,7 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ const {scalePoints, scaleIntensity, pointSize, cScale, cOffset, valueRange, animProg, timeScale, xRange, yRange, zRange, fillValue, maskTexture, maskValue, disablePointScale, - colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ scalePoints: state.scalePoints, scaleIntensity: state.scaleIntensity, pointSize: state.pointSize, @@ -82,6 +82,7 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ maskValue: state.maskValue, disablePointScale: state.disablePointScale, colorScale: state.colorScale, + logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, @@ -152,6 +153,7 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ vertBounds:{value: new THREE.Vector2(yRange[0], yRange[1])}, fillValue: {value: fillValue?? NaN}, colorScale: {value: colorScaleToId(colorScale)}, + logConstant: {value: logConstant}, lowclip: {value: parseColorToVec4(lowclip)}, highclip: {value: parseColorToVec4(highclip)}, useLowclip: {value: useLowclip}, @@ -196,12 +198,13 @@ export const PointCloud = ({textures} : {textures:PCProps} )=>{ uniforms.maskValue.value = maskValue uniforms.aspect.value = shape.x/shape.y; uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.logConstant.value = logConstant; uniforms.lowclip.value = parseColorToVec4(lowclip); uniforms.highclip.value = parseColorToVec4(highclip); uniforms.useLowclip.value = useLowclip; uniforms.useHighclip.value = useHighclip; } - }, [volTexture, depthRatio, depth, height, shape, width, pointSize, colormap, cOffset, cScale, valueRange, scalePoints, scaleIntensity, animProg, xRange, yRange, fillValue, zRange, maskValue, colorScale, lowclip, highclip, useLowclip, useHighclip]); + }, [volTexture, depthRatio, depth, height, shape, width, pointSize, colormap, cOffset, cScale, valueRange, scalePoints, scaleIntensity, animProg, xRange, yRange, fillValue, zRange, maskValue, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]); return ( diff --git a/src/components/plots/Sphere.tsx b/src/components/plots/Sphere.tsx index 0effdf65..65186838 100644 --- a/src/components/plots/Sphere.tsx +++ b/src/components/plots/Sphere.tsx @@ -58,7 +58,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture const {animate, animProg, cOffset, cScale, valueRange, selectTS, nanColor, nanTransparency, sphereDisplacement, sphereResolution, zSlice, ySlice, xSlice, fillValue, borderTexture, maskTexture, maskValue, - getColorIdx, incrementColorIdx} = usePlotStore(useShallow(state=> ({ + getColorIdx, incrementColorIdx, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ animate: state.animate, animProg: state.animProg, cOffset: state.cOffset, @@ -77,6 +77,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture getColorIdx: state.getColorIdx, incrementColorIdx: state.incrementColorIdx, colorScale: state.colorScale, + logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, @@ -119,6 +120,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture displacement: {value: sphereDisplacement}, fillValue: {value: NaN}, colorScale: {value: colorScaleToId(colorScale)}, + logConstant: {value: logConstant}, lowclip: {value: parseColorToVec4(lowclip)}, highclip: {value: parseColorToVec4(highclip)}, useLowclip: {value: useLowclip}, @@ -168,6 +170,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture uniforms.displacement.value = sphereDisplacement uniforms.fillValue.value = fillValue?? NaN uniforms.colorScale.value = colorScaleToId(colorScale) + uniforms.logConstant.value = logConstant uniforms.lowclip.value = parseColorToVec4(lowclip) uniforms.highclip.value = parseColorToVec4(highclip) uniforms.useLowclip.value = useLowclip @@ -181,7 +184,7 @@ export const Sphere = ({textures: propTextures} : {textures: THREE.Data3DTexture if (backMaterial){ updateMaterial(backMaterial) } - },[textures, remapTexture, animProg, colormap, cOffset, cScale, animate, lonBounds, latBounds, nanColor, nanTransparency, sphereDisplacement,valueRange, fillValue, maskValue, valueScales, colorScale, lowclip, highclip, useLowclip, useHighclip]) + },[textures, remapTexture, animProg, colormap, cOffset, cScale, animate, lonBounds, latBounds, nanColor, nanTransparency, sphereDisplacement,valueRange, fillValue, maskValue, valueScales, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) function HandleTimeSeries(event: THREE.Intersection){ diff --git a/src/components/plots/SphereBlocks.tsx b/src/components/plots/SphereBlocks.tsx index e6be1859..6eced112 100644 --- a/src/components/plots/SphereBlocks.tsx +++ b/src/components/plots/SphereBlocks.tsx @@ -34,7 +34,7 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ remapTexture: state.remapTexture }))) const { animProg, cOffset, cScale, nanColor, nanTransparency, sphereDisplacement, offsetNegatives, fillValue, valueRange, maskTexture, maskValue, - colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ + colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state=> ({ animate: state.animate, animProg: state.animProg, cOffset: state.cOffset, @@ -49,6 +49,7 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ maskTexture: state.maskTexture, maskValue: state.maskValue, colorScale: state.colorScale, + logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, @@ -117,6 +118,7 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ displacement: {value: sphereDisplacement}, fillValue: {value: fillValue?? NaN}, colorScale: {value: colorScaleToId(colorScale)}, + logConstant: {value: logConstant}, lowclip: {value: parseColorToVec4(lowclip)}, highclip: {value: parseColorToVec4(highclip)}, useLowclip: {value: useLowclip}, @@ -159,13 +161,14 @@ const SphereBlocks = ({textures: propTextures} : {textures: THREE.Data3DTexture[ uniforms.fillValue.value = fillValue?? NaN uniforms.maskValue.value = maskValue uniforms.colorScale.value = colorScaleToId(colorScale); + uniforms.logConstant.value = logConstant; uniforms.lowclip.value = parseColorToVec4(lowclip); uniforms.highclip.value = parseColorToVec4(highclip); uniforms.useLowclip.value = useLowclip; uniforms.useHighclip.value = useHighclip; } invalidate(); - },[animProg, valueScales, sphereDisplacement, colormap, cScale, cOffset, latBounds, lonBounds, valueRange, offsetNegatives, textures, remapTexture, maskValue, fillValue, colorScale, lowclip, highclip, useLowclip, useHighclip]) + },[animProg, valueScales, sphereDisplacement, colormap, cScale, cOffset, latBounds, lonBounds, valueRange, offsetNegatives, textures, remapTexture, maskValue, fillValue, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]) const nanMaterial = useMemo(()=>new THREE.MeshBasicMaterial({color:nanColor}),[]) nanMaterial.transparent = true; diff --git a/src/components/textures/colormap.tsx b/src/components/textures/colormap.tsx index 79d73833..a27d7a87 100644 --- a/src/components/textures/colormap.tsx +++ b/src/components/textures/colormap.tsx @@ -7,6 +7,7 @@ export const COLOR_SCALE_OPTIONS = [ { label: 'x (Linear)', value: 'identity' }, { label: 'log(x)', value: 'log(x)' }, { label: 'log(1+x)', value: 'log(1+x)' }, + { label: 'log(x+c)', value: 'log(x+c)' }, { label: 'sign(x)*sqrt(abs(x))', value: 'sign(x)*sqrt(abs(x))' }, { label: 'exp(x)/100', value: 'exp(x)/100' }, ] as const; @@ -15,20 +16,27 @@ export function colorScaleToId(colorScale: string): number { switch (colorScale) { case 'log(x)': return 1; case 'log(1+x)': return 2; - case 'sign(x)*sqrt(abs(x))': return 3; - case 'exp(x)/100': return 4; + case 'log(x+c)': return 3; + case 'sign(x)*sqrt(abs(x))': return 4; + case 'exp(x)/100': return 5; case 'identity': default: return 0; } } -export function applyColorScale(x: number, scaleType: string): number { +export function applyColorScale(x: number, scaleType: string, c = 1.0): number { if (scaleType === 'log(x)') { const eps = 0.000001; const clamped = Math.max(x, eps); return (Math.log(clamped) - Math.log(eps)) / (Math.log(1.0 + eps) - Math.log(eps)); } else if (scaleType === 'log(1+x)') { return Math.log(1.0 + Math.max(x, 0.0)) / Math.log(2.0); + } else if (scaleType === 'log(x+c)') { + const safeC = Math.max(c, 0.00001); + const clamped = Math.max(x + safeC, 0.000001); + const num = Math.log(clamped) - Math.log(safeC); + const denom = Math.log(1.0 + safeC) - Math.log(safeC); + return denom !== 0 ? num / denom : x; } else if (scaleType === 'sign(x)*sqrt(abs(x))') { return Math.sign(x) * Math.sqrt(Math.abs(x)); } else if (scaleType === 'exp(x)/100') { diff --git a/src/components/textures/shaders/flatFrag.glsl b/src/components/textures/shaders/flatFrag.glsl index f0fc220f..6bec8e3c 100644 --- a/src/components/textures/shaders/flatFrag.glsl +++ b/src/components/textures/shaders/flatFrag.glsl @@ -22,6 +22,7 @@ uniform vec2 threshold; uniform int maskValue; uniform float fillValue; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; @@ -32,7 +33,7 @@ out vec4 Color; #define epsilon 0.0001 #define PI 3.14159265 -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -40,8 +41,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -141,8 +148,21 @@ void main() { float range = max(threshold.y - threshold.x, 0.0001); float normS = clamp((strength - threshold.x) / range, 0.0, 1.0); - float scaledS = applyColorScale(normS, colorScale); - float sampLoc = min(scaledS * cScale + cOffset, 0.995); + float scaledS = applyColorScale(normS, colorScale, logConstant); + float rawSampLoc = scaledS * cScale + cOffset; + + if (rawSampLoc < 0.0) { + if (useLowclip) Color = lowclip; + else Color = vec4(texture(cmap, vec2(0.0, 0.5)).rgb, 1.0); + return; + } + if (rawSampLoc > 1.0) { + if (useHighclip) Color = highclip; + else Color = vec4(texture(cmap, vec2(0.995, 0.5)).rgb, 1.0); + return; + } + + float sampLoc = clamp(rawSampLoc, 0.0, 0.995); Color = vec4(texture(cmap, vec2(sampLoc, 0.5)).rgb, 1.0); // float check = float(texture(remapTexture,texCoord.xy).g >= 0.); // Color = vec4(check, 0., 0. , 1.); diff --git a/src/components/textures/shaders/fragmentOpt.glsl b/src/components/textures/shaders/fragmentOpt.glsl index 7889773a..c21f4b01 100644 --- a/src/components/textures/shaders/fragmentOpt.glsl +++ b/src/components/textures/shaders/fragmentOpt.glsl @@ -31,6 +31,7 @@ uniform int maskValue; uniform vec2 latBounds; uniform vec2 lonBounds; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; @@ -39,7 +40,7 @@ uniform bool useHighclip; #define epsilon 0.000001 #define pi 3.1415926535 -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -47,8 +48,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -167,34 +174,38 @@ void main() { t -= coarseDelta; continue; } - if (d == 1. || abs(d - fillValue) < 0.005){ + if (d == 1. || abs(d - fillValue) < 0.005) { accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; alphaAcc += pow(nanAlpha, 5.); - } else if (d < threshold.x) { - if (useLowclip) { - accumColor.rgb += (1.0 - alphaAcc) * lowclip.a * lowclip.rgb; - alphaAcc += lowclip.a * (1.0 - alphaAcc); - } - } else if (d > threshold.y) { - if (useHighclip) { - accumColor.rgb += (1.0 - alphaAcc) * highclip.a * highclip.rgb; - alphaAcc += highclip.a * (1.0 - alphaAcc); - } } else { float range = max(threshold.y - threshold.x, 0.0001); float normD = clamp((d - threshold.x) / range, 0.0, 1.0); - float scaledD = applyColorScale(normD, colorScale); - float sampLoc = min(scaledD * cScale + cOffset, 0.99); - vec4 col = texture(cmap, vec2(sampLoc, 0.5)); - float alpha; - if (useClipScale) { - float normalizedOpacity = clamp(scaledD, 0.0, 1.0); - alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); + float scaledD = applyColorScale(normD, colorScale, logConstant); + float rawSampLoc = scaledD * cScale + cOffset; + + if (d < threshold.x || rawSampLoc < 0.0) { + if (useLowclip) { + accumColor.rgb += (1.0 - alphaAcc) * lowclip.a * lowclip.rgb; + alphaAcc += lowclip.a * (1.0 - alphaAcc); + } + } else if (d > threshold.y || rawSampLoc > 1.0) { + if (useHighclip) { + accumColor.rgb += (1.0 - alphaAcc) * highclip.a * highclip.rgb; + alphaAcc += highclip.a * (1.0 - alphaAcc); + } } else { - alpha = pow(max(sampLoc, 0.001), transparency * opacityMag); + float sampLoc = clamp(rawSampLoc, 0.0, 0.99); + vec4 col = texture(cmap, vec2(sampLoc, 0.5)); + float alpha; + if (useClipScale) { + float normalizedOpacity = clamp(scaledD, 0.0, 1.0); + alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); + } else { + alpha = pow(max(sampLoc, 0.001), transparency * opacityMag); + } + accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; + alphaAcc += alpha * (1.0 - alphaAcc); } - accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; - alphaAcc += alpha * (1.0 - alphaAcc); } if (alphaAcc >= 1.0) break; diff --git a/src/components/textures/shaders/pointFrag.glsl b/src/components/textures/shaders/pointFrag.glsl index c218a057..2f542e51 100644 --- a/src/components/textures/shaders/pointFrag.glsl +++ b/src/components/textures/shaders/pointFrag.glsl @@ -10,12 +10,13 @@ uniform float fillValue; uniform vec3 nanColor; uniform float nanAlpha; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; uniform bool useHighclip; -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -23,8 +24,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -49,8 +56,21 @@ void main() { float range = max(valueRange.y - valueRange.x, 0.0001); float normVal = clamp((vValue - valueRange.x) / range, 0.0, 1.0); - float scaledVal = applyColorScale(normVal, colorScale); - float sampLoc = min(scaledVal * cScale + cOffset, 0.99); + float scaledVal = applyColorScale(normVal, colorScale, logConstant); + float rawSampLoc = scaledVal * cScale + cOffset; + + if (rawSampLoc < 0.0) { + if (useLowclip) Color = lowclip; + else Color = vec4(texture(cmap, vec2(0.0, 0.5)).rgb, 1.0); + return; + } + if (rawSampLoc > 1.0) { + if (useHighclip) Color = highclip; + else Color = vec4(texture(cmap, vec2(0.995, 0.5)).rgb, 1.0); + return; + } + + float sampLoc = clamp(rawSampLoc, 0.0, 0.995); vec4 color = texture(cmap, vec2(sampLoc, 0.5)); color.a = 1.; Color = color; diff --git a/src/components/textures/shaders/sphereBlocksFrag.glsl b/src/components/textures/shaders/sphereBlocksFrag.glsl index 9782090b..051af152 100644 --- a/src/components/textures/shaders/sphereBlocksFrag.glsl +++ b/src/components/textures/shaders/sphereBlocksFrag.glsl @@ -6,6 +6,7 @@ uniform float fillValue; uniform vec3 nanColor; uniform float nanAlpha; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; @@ -15,7 +16,7 @@ in float vStrength; out vec4 Color; -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -23,8 +24,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -50,9 +57,21 @@ void main() { float range = max(threshold.y - threshold.x, 0.0001); float normS = clamp((strength - threshold.x) / range, 0.0, 1.0); - float scaledS = applyColorScale(normS, colorScale); - float sampLoc = min(scaledS * cScale + cOffset, 0.996); - vec3 sampColor = texture(cmap, vec2(sampLoc, 0.5)).rgb; + float scaledS = applyColorScale(normS, colorScale, logConstant); + float rawSampLoc = scaledS * cScale + cOffset; + if (rawSampLoc < 0.0) { + if (useLowclip) Color = lowclip; + else Color = vec4(texture(cmap, vec2(0.0, 0.5)).rgb, 1.0); + return; + } + if (rawSampLoc > 1.0) { + if (useHighclip) Color = highclip; + else Color = vec4(texture(cmap, vec2(0.995, 0.5)).rgb, 1.0); + return; + } + + float sampLoc = clamp(rawSampLoc, 0.0, 0.995); + vec3 sampColor = texture(cmap, vec2(sampLoc, 0.5)).rgb; Color = vec4(sampColor, 1.0); } \ No newline at end of file diff --git a/src/components/textures/shaders/sphereFrag.glsl b/src/components/textures/shaders/sphereFrag.glsl index 5edb83a3..cf5c49ba 100644 --- a/src/components/textures/shaders/sphereFrag.glsl +++ b/src/components/textures/shaders/sphereFrag.glsl @@ -25,6 +25,7 @@ uniform float nanAlpha; uniform float fillValue; uniform int maskValue; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; @@ -33,7 +34,7 @@ uniform bool useHighclip; #define pi 3.141592653 #define epsilon 0.0001 -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -41,8 +42,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -143,9 +150,16 @@ void main(){ } else { float range = max(threshold.y - threshold.x, 0.0001); float normS = clamp((strength - threshold.x) / range, 0.0, 1.0); - float scaledS = applyColorScale(normS, colorScale); - float sampLoc = min(scaledS * cScale + cOffset, 0.99); - color = vec4(texture(cmap, vec2(sampLoc, 0.5)).rgb, 1.0); + float scaledS = applyColorScale(normS, colorScale, logConstant); + float rawSampLoc = scaledS * cScale + cOffset; + if (rawSampLoc < 0.0) { + color = useLowclip ? lowclip : vec4(texture(cmap, vec2(0.0, 0.5)).rgb, 1.0); + } else if (rawSampLoc > 1.0) { + color = useHighclip ? highclip : vec4(texture(cmap, vec2(0.995, 0.5)).rgb, 1.0); + } else { + float sampLoc = clamp(rawSampLoc, 0.0, 0.995); + color = vec4(texture(cmap, vec2(sampLoc, 0.5)).rgb, 1.0); + } } if (maskValue != 0){ diff --git a/src/components/textures/shaders/volFragment.glsl b/src/components/textures/shaders/volFragment.glsl index dbf8cad3..90879695 100644 --- a/src/components/textures/shaders/volFragment.glsl +++ b/src/components/textures/shaders/volFragment.glsl @@ -31,6 +31,7 @@ uniform int maskValue; uniform vec2 latBounds; uniform vec2 lonBounds; uniform int colorScale; +uniform float logConstant; uniform vec4 lowclip; uniform vec4 highclip; uniform bool useLowclip; @@ -39,7 +40,7 @@ uniform bool useHighclip; #define epsilon 0.000001 #define pi 3.1415926535 -float applyColorScale(float x, int scaleType) { +float applyColorScale(float x, int scaleType, float c) { if (scaleType == 1) { float eps = 0.000001; float clamped = max(x, eps); @@ -47,8 +48,14 @@ float applyColorScale(float x, int scaleType) { } else if (scaleType == 2) { return log(1.0 + max(x, 0.0)) / log(2.0); } else if (scaleType == 3) { - return sign(x) * sqrt(abs(x)); + float safeC = max(c, 0.00001); + float clamped = max(x + safeC, 0.000001); + float num = log(clamped) - log(safeC); + float denom = log(1.0 + safeC) - log(safeC); + return denom != 0.0 ? num / denom : x; } else if (scaleType == 4) { + return sign(x) * sqrt(abs(x)); + } else if (scaleType == 5) { return (exp(x) - 1.0) / (exp(1.0) - 1.0); } return x; @@ -158,31 +165,35 @@ void main() { if (d == 1. || abs(d - fillValue) < 0.005) { accumColor.rgb += (1.0 - alphaAcc) * pow(nanAlpha, 5.) * nanColor.rgb; alphaAcc += pow(nanAlpha, 5.); - } else if (d < threshold.x) { - if (useLowclip) { - accumColor.rgb += (1.0 - alphaAcc) * lowclip.a * lowclip.rgb; - alphaAcc += lowclip.a * (1.0 - alphaAcc); - } - } else if (d > threshold.y) { - if (useHighclip) { - accumColor.rgb += (1.0 - alphaAcc) * highclip.a * highclip.rgb; - alphaAcc += highclip.a * (1.0 - alphaAcc); - } } else { float range = max(threshold.y - threshold.x, 0.0001); float normD = clamp((d - threshold.x) / range, 0.0, 1.0); - float scaledD = applyColorScale(normD, colorScale); - float sampLoc = min(scaledD * cScale + cOffset, 0.99); - vec4 col = texture(cmap, vec2(sampLoc, 0.5)); - float alpha; - if (useClipScale) { - float normalizedOpacity = clamp(scaledD, 0.0, 1.0); - alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); + float scaledD = applyColorScale(normD, colorScale, logConstant); + float rawSampLoc = scaledD * cScale + cOffset; + + if (d < threshold.x || rawSampLoc < 0.0) { + if (useLowclip) { + accumColor.rgb += (1.0 - alphaAcc) * lowclip.a * lowclip.rgb; + alphaAcc += lowclip.a * (1.0 - alphaAcc); + } + } else if (d > threshold.y || rawSampLoc > 1.0) { + if (useHighclip) { + accumColor.rgb += (1.0 - alphaAcc) * highclip.a * highclip.rgb; + alphaAcc += highclip.a * (1.0 - alphaAcc); + } } else { - alpha = pow(max(sampLoc, 0.001), transparency * opacityMag); + float sampLoc = clamp(rawSampLoc, 0.0, 0.99); + vec4 col = texture(cmap, vec2(sampLoc, 0.5)); + float alpha; + if (useClipScale) { + float normalizedOpacity = clamp(scaledD, 0.0, 1.0); + alpha = pow(max(normalizedOpacity, 0.001), transparency * opacityMag); + } else { + alpha = pow(max(sampLoc, 0.001), transparency * opacityMag); + } + accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; + alphaAcc += alpha * (1.0 - alphaAcc); } - accumColor.rgb += (1.0 - alphaAcc) * alpha * col.rgb; - alphaAcc += alpha * (1.0 - alphaAcc); } if (alphaAcc >= 1.0) break; diff --git a/src/components/ui/Colorbar.tsx b/src/components/ui/Colorbar.tsx index e8d09fd8..6f593bb4 100644 --- a/src/components/ui/Colorbar.tsx +++ b/src/components/ui/Colorbar.tsx @@ -56,12 +56,13 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec variable: state.variable, scalingFactor: state.scalingFactor }))) - const {cScale, cOffset, setCScale, setCOffset, colorScale, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ + const {cScale, cOffset, setCScale, setCOffset, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip} = usePlotStore(useShallow(state => ({ cScale: state.cScale, cOffset: state.cOffset, setCScale: state.setCScale, setCOffset: state.setCOffset, colorScale: state.colorScale, + logConstant: state.logConstant, lowclip: state.lowclip, highclip: state.highclip, useLowclip: state.useLowclip, @@ -111,11 +112,11 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec const locs = linspace(0, 100, tickCount); const vals = locs.map(loc => { const t = loc / 100; - const scaledT = applyColorScale(t, colorScale); + const scaledT = applyColorScale(t, colorScale, logConstant); return newMin + (newMax - newMin) * scaledT; }); return [locs, vals] - },[ tickCount, newMin, newMax, colorScale]) + },[ tickCount, newMin, newMax, colorScale, logConstant]) // Mouse move handler const handleMouseMove = (e: MouseEvent) => { @@ -200,7 +201,7 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec for (let x = 0; x < mainWidth; x++) { const normX = x / mainWidth; - const scaledT = applyColorScale(normX, colorScale); + const scaledT = applyColorScale(normX, colorScale, logConstant); const colorIndex = Math.min(Math.floor(scaledT * (colors.length - 1)), colors.length - 1); ctx.fillStyle = colors[colorIndex] || '#000'; ctx.fillRect(startX + x, 0, 1, height); @@ -212,7 +213,7 @@ const Colorbar = ({units, metadata, valueScales} : {units: string, metadata: Rec } } } - }, [colors, colorScale, lowclip, highclip, useLowclip, useHighclip]); + }, [colors, colorScale, logConstant, lowclip, highclip, useLowclip, useHighclip]); const analysisString = useMemo(()=>{ if (analysisMode){ const twoVar = variable2 != "Default"; diff --git a/src/components/ui/MainPanel/Colormaps.tsx b/src/components/ui/MainPanel/Colormaps.tsx index c620fa2a..727c10eb 100644 --- a/src/components/ui/MainPanel/Colormaps.tsx +++ b/src/components/ui/MainPanel/Colormaps.tsx @@ -38,7 +38,7 @@ const Colormaps = () => { const [hoveredCmap, setHoveredCmap] = useState(null); const [selectedCategory, setSelectedCategory] = useState(''); const [showNames, setShowNames] = useState(true); - const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap } = useGlobalStore( + const { colormap, setColormap, colormapName, flipColormap, setColormapName, setFlipColormap, valueScales } = useGlobalStore( useShallow((state) => ({ setColormap: state.setColormap, colormap: state.colormap, @@ -46,12 +46,15 @@ const Colormaps = () => { flipColormap: state.flipColormap, setColormapName: state.setColormapName, setFlipColormap: state.setFlipColormap, + valueScales: state.valueScales, })) ); - const { colorScale, setColorScale, lowclip, setLowclip, highclip, setHighclip, useLowclip, setUseLowclip, useHighclip, setUseHighclip } = usePlotStore( + const { colorScale, setColorScale, logConstant, setLogConstant, lowclip, setLowclip, highclip, setHighclip, useLowclip, setUseLowclip, useHighclip, setUseHighclip } = usePlotStore( useShallow((state) => ({ colorScale: state.colorScale, setColorScale: state.setColorScale, + logConstant: state.logConstant, + setLogConstant: state.setLogConstant, lowclip: state.lowclip, setLowclip: state.setLowclip, highclip: state.highclip, @@ -301,14 +304,34 @@ const Colormaps = () => { - {COLOR_SCALE_OPTIONS.map((opt) => ( - - {opt.label} - - ))} + {COLOR_SCALE_OPTIONS.map((opt) => { + const isDisabled = opt.value === 'log(x)' && valueScales && valueScales.minVal < 0; + return ( + + {opt.label} {isDisabled ? '(min < 0)' : ''} + + ); + })} + {colorScale === 'log(x+c)' && ( +
+ c = + setLogConstant(parseFloat(e.target.value) || 1.0)} + className="w-20 px-2 py-0.5 text-xs rounded border border-neutral-300 dark:border-neutral-700 bg-transparent" + /> +
+ )}