From 4aa103ab6f5ed4ef3ccaacc3ee60201e78bebe7c Mon Sep 17 00:00:00 2001 From: Jeran Date: Mon, 5 Oct 2026 11:13:24 +0200 Subject: [PATCH 01/61] First stash --- src/components/plots/DataCube.tsx | 65 +------- src/components/plots/MorphingPoints.tsx | 1 - src/components/plots/Plot.tsx | 18 +-- src/components/textures/TSL/rayMarcher.ts | 87 +++++++++++ .../textures/TSL/utils/commonHelpers.ts | 142 ++++++++++++++++++ .../textures/TSL/utils/commonUniforms.ts | 33 ++++ 6 files changed, 275 insertions(+), 71 deletions(-) create mode 100644 src/components/textures/TSL/rayMarcher.ts create mode 100644 src/components/textures/TSL/utils/commonHelpers.ts create mode 100644 src/components/textures/TSL/utils/commonUniforms.ts diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 6ffe6fd24..92338ca74 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef } from 'react' -import * as THREE from 'three' +import * as THREE from 'three/webgpu' import { vertexShader, rayMarchFrag, orthoVertex , ddaFrag} from '@/components/textures/shaders'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; @@ -10,6 +10,7 @@ import { ColumnMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; import { functionInjector } from '../ui/Elements/ColorAdjuster'; +import { raymarchClouds } from '../textures/TSL/rayMarcher'; interface DataCubeProps { volTexture: THREE.Data3DTexture[] | THREE.DataTexture[] | undefined, } @@ -29,64 +30,12 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { },[remapTexture, dataShape]) const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms: { - modelViewMatrixInverse: { value: new THREE.Matrix4() }, // Used for Orthographic RayMarcher - map: { value: volTexture}, - dataShape: {value: gridShape}, - remapTexture: { value: remapTexture?? remapBorders}, - scale: {value: shape}, - flatBounds:{value: new THREE.Vector4(-xRange[1],-xRange[0],zRange[0] * timeRatio, zRange[1] * timeRatio)}, - vertBounds:{value: flipY - ? new THREE.Vector2(yRange[0]*aspectRatio,yRange[1]*aspectRatio) - : new THREE.Vector2(yRange[0]*aspectRatio,yRange[1]*aspectRatio) - }, - steps: { value: quality }, - transparency: {value: transparency}, - revTransparency: {value: revTransparency}, - opacityMag: {value: vTransferScale}, - useClipScale: {value: vTransferRange}, - ...uniforms - }, - defines: { - USE_VORIGIN: 1, - USE_VDIRECTION: 1, - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: useOrtho ? orthoVertex : vertexShader, - fragmentShader: useRayMarch ? functionInjector(rayMarchFrag, colorScale): functionInjector(ddaFrag, colorScale), - transparent: true, - blending: THREE.NormalBlending, - depthWrite: false, - side: useOrtho ? THREE.FrontSide : THREE.BackSide, - }),[useRayMarch, useOrtho, volTexture, colorScale, remapTexture]); - + const shaderMaterial = useMemo(()=> new THREE.MeshBasicNodeMaterial(),[useRayMarch, useOrtho, volTexture, colorScale, remapTexture]); + shaderMaterial.colorNode = raymarchClouds() + shaderMaterial.side = THREE.BackSide; + shaderMaterial.transparent = true; const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); - updateCommonUniforms(shaderMaterial) - useEffect(() => { - if (shaderMaterial) { - const uniforms = shaderMaterial.uniforms - uniforms.dataShape.value = gridShape; - uniforms.scale.value = shape; - uniforms.flatBounds.value.set(-xRange[1], -xRange[0], zRange[0] * timeRatio, zRange[1] * timeRatio); - flipY ? uniforms.vertBounds.value.set(-yRange[1] * aspectRatio, -yRange[0] * aspectRatio) - : uniforms.vertBounds.value.set(yRange[0] * aspectRatio, yRange[1] * aspectRatio); - uniforms.steps.value = quality; - uniforms.transparency.value = transparency; - uniforms.revTransparency.value = revTransparency; - uniforms.opacityMag.value = vTransferScale; - uniforms.useClipScale.value = vTransferRange; - invalidate() // Needed because Won't trigger re-render if camera is stationary. - } - }, [shape, gridShape, xRange, yRange, zRange, aspectRatio, quality, transparency, revTransparency, vTransferScale, vTransferRange, flipY]); - useFrame(({camera})=>{ // This calculates InverseModel matrix for the orthographic raymarcher - if (!useOrtho || !meshRef.current || !shaderMaterial) return; - meshRef.current.modelViewMatrix.multiplyMatrices(camera.matrixWorldInverse, meshRef.current.matrixWorld); - shaderMaterial.uniforms.modelViewMatrixInverse.value - .copy(meshRef.current.modelViewMatrix) - .invert(); - }) + return ( diff --git a/src/components/plots/MorphingPoints.tsx b/src/components/plots/MorphingPoints.tsx index a08637673..9320a5d23 100644 --- a/src/components/plots/MorphingPoints.tsx +++ b/src/components/plots/MorphingPoints.tsx @@ -219,7 +219,6 @@ export const LandingShapes = () =>{ - diff --git a/src/components/plots/Plot.tsx b/src/components/plots/Plot.tsx index 3b3fd869c..56ef76dd2 100644 --- a/src/components/plots/Plot.tsx +++ b/src/components/plots/Plot.tsx @@ -1,6 +1,6 @@ import { OrbitControls, useTexture } from '@react-three/drei'; import React, { useMemo, useRef, useState, useEffect } from 'react'; -import * as THREE from 'three'; +import * as THREE from 'three/webgpu'; import { PointCloud, DataCube, FlatMap, Sphere, CountryBorders, AxisLines, SphereBlocks, FlatBlocks, KeyFramePreviewer } from '@/components/plots'; import { Canvas, invalidate, useThree } from '@react-three/fiber'; import { ArrayToTexture, createDataTexture } from '@/components/textures'; @@ -167,10 +167,6 @@ const Plot = () => { const {analysisMode, analysisArray, useEditor} = useAnalysisStore(useShallow(s => ({ analysisMode: s.analysisMode, analysisArray: s.analysisArray, useEditor: s.useEditor }))) - const coords = useRef([0,0]) - const val = useRef(0) - const [showInfo, setShowInfo] = useState(false) - const [loc, setLoc] = useState([0,0]) //DATA LOADING const {show, stableMetadata} = useDataFetcher() @@ -202,12 +198,6 @@ const Plot = () => { } setStatus(null) },[useF16Textures]) - const infoSetters = useMemo(()=>({ - setLoc, - setShowInfo, - coords, - val - }),[]) const prevUniqueRep = useRef(false); ` Point cloud and sphere need unique reprojections. However, reprojecting from volume @@ -248,7 +238,11 @@ const Plot = () => { { + const renderer = new THREE.WebGPURenderer(props as any) + await renderer.init() + return renderer + }} dpr={[DPR,DPR]} > diff --git a/src/components/textures/TSL/rayMarcher.ts b/src/components/textures/TSL/rayMarcher.ts new file mode 100644 index 000000000..88df98c60 --- /dev/null +++ b/src/components/textures/TSL/rayMarcher.ts @@ -0,0 +1,87 @@ +import { Fn, vec4, vec3, time, If, smoothstep, exp, Break, modelWorldMatrixInverse, triNoise3D, pmremTexture, modelWorldMatrix } from 'three/tsl'; +import { RaymarchingBox } from 'three/addons/tsl/utils/Raymarching.js'; + +// Volumetric cloud density calculator (simplified ellipsoid shape with warping and edge noise erosion) +const getCloudDensity = Fn( ( [ p ] : [ any ]) => { + + // 1. Warp coordinate space to deform the geometric boundaries organically (Domain Warping) + const warp = triNoise3D( p, 0.5, time.mul( 0.5 ) ); + const pWarped = p.add( vec3( warp ).sub( 0.5 ).mul( 0.4 ) ); + + // 2. Base Ellipsoid Mask (wider than it is tall to look like a flat cumulus cloud) + const baseMask = smoothstep( 0.48, 0.22, pWarped.mul( vec3( 1, 1.3, 1 ) ).length() ); + + // 3. Volumetric details with slow wind drift + const pNoise = pWarped.add( vec3( time.mul( 0.05 ), 0, time.mul( 0.02 ) ) ); + const noiseVal = triNoise3D( pNoise.mul( 1.2 ), 1.2, time.mul( 0.6 ) ); + + // Erode only the edges of the cloud, keeping the center core solid + const shape = baseMask.sub( noiseVal.mul( baseMask.pow( 2 ).oneMinus().mul( 0.48 ) ) ); + + // Soft threshold to get beautiful rounded boundaries with smooth fade-out + return smoothstep( - 0.02, 0.35, shape ); + +} ); + +export const raymarchClouds = Fn( () => { + + const steps = 48; + + const finalColor = vec4( 0 ); + + // Direct light source (constant direction from top-right-front, matching the reference image) + const lightDir = vec3( 1, 1.2, 0.8 ).normalize(); + const localLightDir = modelWorldMatrixInverse.mul( vec4( lightDir, 0 ) ).xyz.normalize(); + + // Direct light color matching the reference sun light + const directLightColor = vec3( 0.5, 1.0, 1.4 ); + + RaymarchingBox( steps, ( { positionRay, stepSize } ) => { + + const density = getCloudDensity( positionRay ); + + If( density.greaterThan( 0.01 ), () => { + + // Shadow ray: sample density offset towards the light source in local space + const shadowPos = positionRay.add( localLightDir.mul( 0.08 ) ); + const shadowDensity = getCloudDensity( shadowPos ); + + // Dual-Lobe Beer's Law for realistic multiple scattering & light penetration + const shadowVal = shadowDensity.mul( 10 ); + const transmittance = exp( shadowVal.negate() ).mul( 0.5 ).add( exp( shadowVal.mul( 0.1 ).negate() ).mul( 0.65 ) ); + + // Calculate local normal based on positionRay relative to cloud center + const normal = positionRay.normalize(); + + // Transform normal to world space so environment directions align correctly with the sky + const worldNormal = modelWorldMatrix.mul( vec4( normal, 0 ) ).xyz.normalize(); + + // Sample ambient light dynamically in the direction of the world normal from the environment map (IBL) + const ambientLightColor = vec3(0.5); + + // Combine direct light and ambient sky contribution (offset positionRay.y by 0.5 to keep factors positive) + const directLight = directLightColor.mul( transmittance ); + const ambientLight = ambientLightColor.mul( positionRay.y.add( 0.5 ) ).mul( density ); + const cloudColor = directLight.add( ambientLight ); + + // Front-to-back blending with accumulated color (higher opacity for solid volume appearance) + const alpha = density.mul( stepSize ).mul( 8 ); + const colSample = cloudColor.mul( alpha ); + + finalColor.rgb.addAssign( finalColor.a.oneMinus().mul( colSample ) ); + finalColor.a.addAssign( finalColor.a.oneMinus().mul( alpha ) ); + + // Early loop termination if cloud gets opaque + If( finalColor.a.greaterThanEqual( 0.95 ), () => { + + Break(); + + } ); + + } ); + + } ); + + return finalColor; + +} ); diff --git a/src/components/textures/TSL/utils/commonHelpers.ts b/src/components/textures/TSL/utils/commonHelpers.ts new file mode 100644 index 000000000..4404f4f8d --- /dev/null +++ b/src/components/textures/TSL/utils/commonHelpers.ts @@ -0,0 +1,142 @@ +import { Fn, min, max, clamp, abs, round, mix, select, + texture, floatBitsToUint, uint, fract, float, vec2, vec3, + vec4, If } from 'three/tsl'; +import { valueRange, resolution, bottomLeft, bottomRight, topLeft, + mixMode, lonBounds, latBounds, is360, remapBorders } from './commonUniforms'; + +const IS_FLAT = true; // or false based on your setup + +// Create an array of 12 texture uniform nodes +const maps = Array.from({ length: 12 }, (_, i) => { + const tex = myThreeTexturesArray[i]; // Your THREE.Texture or THREE.Data3DTexture + + return IS_FLAT + ? texture(tex) // Creates 2D sampler uniform node + : texture3D(tex); // Creates 3D sampler uniform node +}); + +// --- TEXTURE SAMPLERS ---// +const sampleMap = Fn(([p, index]: [any, any]) => { + const result = vec4(0).toVar(); + maps.forEach((map, i) => { + If(index.equal(i), () => { + result.assign(texture(map, p)); + }); + }); + return result; + }); + +export const sample1 = Fn(([p, index]: [any, any]) => sampleMap(p, index).r); +export const sample2 = Fn(([p, index]: [any, any]) => sampleMap(p, index).rg); + +export const sample2ToOrder = Fn(([p, index, variable]: [any, any, any]) => { +const biVar = sample2(p, index); +return select(variable.equal(0), biVar, biVar.gr); +}); + +// --- GEOHELPER --- // +export const realCoords = Fn(([uv]: [any]) => { + // Radians -> fraction of a full turn. Longitudes in -180..180 need shifting by 0.5. + const lonRange = lonBounds.div(2 * Math.PI); + const normalizedLon = select(is360, lonRange, lonRange.add(0.5)); + + // Latitude: -PI/2..PI/2 -> 0..1 + const normalizedLat = latBounds.div(Math.PI).add(0.5); + + const lonScale = normalizedLon.y.sub(normalizedLon.x); + const latScale = normalizedLat.y.sub(normalizedLat.x); + + const u = uv.x.mul(lonScale).add(normalizedLon.x); + const v = uv.y.mul(latScale).add(normalizedLat.x); + + return vec2(u, v); +}); + + +// --- BIVARIATE COLORS --- // +export const lerpColors = Fn(([A, B, fac]: [any, any, any]) => { + const steps = resolution.sub(1); + const snapped = round(fac.mul(steps)).div(steps); + return mix(A, B, snapped); +}); + +export const darkenColors = (A: any, B: any) => min(A, B); +export const lightenColors = (A: any, B: any) => max(A, B); +export const multiplyColors = (A: any, B: any) => clamp(A.mul(B), 0, 1); +export const differenceColors = (A: any, B: any) => abs(A.sub(B)); + +export const colorMixer = Fn(([A, B]: [any, any]) => { + const bottomColor = lerpColors(bottomLeft, bottomRight, A); + const leftColor = lerpColors(bottomLeft, topLeft, B); + + // Pick the blend mode (falls back to darken) + return select( + mixMode.equal(1), lightenColors(bottomColor, leftColor), + select( + mixMode.equal(2), multiplyColors(bottomColor, leftColor), + select( + mixMode.equal(3), differenceColors(bottomColor, leftColor), + darkenColors(bottomColor, leftColor), + ), + ), + ); +}); + +export function createBivariateColor() { + return Fn(([p, index]: [any, any]) => { + const biValues = sample2(p, index); + const hasNaN = isNaNBits(biValues.r).or(isNaNBits(biValues.g)); + + const color = select(hasNaN, vec3(0, 0, 0), colorMixer(biValues.r, biValues.g)); + return vec4(color, select(hasNaN, float(1), float(0))); + }); +} + + +// --- VALUE SCALING --- // +export const denorm = (x: any) => x.mul(valueRange.y.sub(valueRange.x)).add(valueRange.x); +export const norm = (x: any) => x.sub(valueRange.x).div(valueRange.y.sub(valueRange.x)); +export const rescaler = (x: any) => x; + +export function reprojector(texCoord: any, opts: Pick) { + const { remapTexture, isFlat, reproject } = opts; + + let newTexCoord = texCoord; + let maskUV: any; + let valid: any; + + if (reproject) { + // Each output pixel looks up where it comes from in the source data + const remap = texture(remapTexture, texCoord.xy).rgb; + + newTexCoord = isFlat ? remap.rg : vec3(remap.rg, texCoord.z); + maskUV = realCoords(remap.rg); + valid = remap.b.greaterThan(0.5); + } else { + // Reprojected data is already -180..180, so no adjusting needed. + const originalCoord = texCoord.xy; + maskUV = realCoords(originalCoord); + + // Regularly gridded data: the remap texture's .ba channels hold the mask coords. + // (Original note: not certain the y-flip is robust.) + const flipped = vec2(originalCoord.x, float(1).sub(originalCoord.y)); + const remappedMask = texture(remapTexture, flipped).ba; + maskUV = select(remapBorders, remappedMask, maskUV); + + valid = float(1).greaterThan(0.5); // always true + } + + // For 0–360 data, wrap longitude by half a turn + const wrappedU = fract(maskUV.x.add(0.5)); + maskUV = vec2(select(is360, wrappedU, maskUV.x), maskUV.y); + + return { texCoord: newTexCoord, maskUV, valid }; +} + +// --- NANNERS ---// +export const isNaNBits = Fn(([x]: [any]) => { + const bits = floatBitsToUint(x); + const exponent = bits.bitAnd(uint(0x7f800000)); + const mantissa = bits.bitAnd(uint(0x007fffff)); + return exponent.equal(uint(0x7f800000)).and(mantissa.notEqual(uint(0))); +}); \ No newline at end of file diff --git a/src/components/textures/TSL/utils/commonUniforms.ts b/src/components/textures/TSL/utils/commonUniforms.ts new file mode 100644 index 000000000..73a9eb0a0 --- /dev/null +++ b/src/components/textures/TSL/utils/commonUniforms.ts @@ -0,0 +1,33 @@ +import { Data3DTexture, DataTexture, Texture } from 'three/webgpu'; +import { texture3D, texture, uniform, uniformArray } from 'three/tsl'; + + +export const maskTexture = texture( new DataTexture() ); +export const cmap = texture( new DataTexture() ); +export const remapTexture = texture( new DataTexture() ); +export const borderTexture = texture( new Texture() ); +export const remapBorders = uniform( 'bool' ); +export const useBorderTexture = uniform( 'bool' ); +export const borderWidth = uniform( 'float' ); +export const borderColor = uniform( 'vec3' ); +export const textureDepths = uniform( 'vec3' ); +export const bivariate = uniform( 'bool' ); +export const bottomLeft = uniform( 'vec3' ); +export const topLeft = uniform( 'vec3' ); +export const bottomRight = uniform( 'vec3' ); +export const resolution = uniform( 'float' ); +export const mixMode = uniform( 'int' ); +export const bivariateSelection = uniform( 'int' ); +export const is360 = uniform( 'bool' ); +export const cOffset = uniform( 'float' ); +export const cScale = uniform( 'float' ); +export const threshold = uniform( 'vec2' ); +export const animateProg = uniform( 'float' ); +export const nanAlpha = uniform( 'float' ); +export const nanColor = uniform( 'vec3' ); +export const maskValue = uniform( 'int' ); +export const fillValue = uniform( 'float' ); +export const latBounds = uniform( 'vec2' ); +export const lonBounds = uniform( 'vec2' ); +export const valueRange = uniform( 'vec2' ); +export const useF16 = uniform( 'bool' ); \ No newline at end of file From 4a0c11244e827299bfbd58940d679454440d0e45 Mon Sep 17 00:00:00 2001 From: Jeran Date: Mon, 5 Oct 2026 13:29:54 +0200 Subject: [PATCH 02/61] Antoher stash --- src/components/plots/DataCube.tsx | 13 +- src/components/plots/FlatMap.tsx | 23 +- src/components/textures/TSL/rayMarcher.ts | 281 ++++++++++++------ .../textures/TSL/utils/commonHelpers.ts | 35 +-- .../textures/TSL/utils/commonUniforms.ts | 7 +- .../textures/TSL/utils/volumeUniforms.ts | 10 + 6 files changed, 229 insertions(+), 140 deletions(-) create mode 100644 src/components/textures/TSL/utils/volumeUniforms.ts diff --git a/src/components/plots/DataCube.tsx b/src/components/plots/DataCube.tsx index 92338ca74..899d1a4b7 100644 --- a/src/components/plots/DataCube.tsx +++ b/src/components/plots/DataCube.tsx @@ -10,11 +10,12 @@ import { ColumnMeshes } from './TransectMeshes'; import { usePaddedTextures } from '@/hooks/usePaddedTextures'; import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; import { functionInjector } from '../ui/Elements/ColorAdjuster'; -import { raymarchClouds } from '../textures/TSL/rayMarcher'; +import { rayMarchingMaterial } from '../textures/TSL/rayMarcher'; interface DataCubeProps { volTexture: THREE.Data3DTexture[] | THREE.DataTexture[] | undefined, } + export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { const volTexture = usePaddedTextures(propVolTexture); const {shape, flipY, remapTexture, remapBorders, dataShape} = useGlobalStore(useShallow(s => s)) //We have to useShallow when returning an object instead of a state. I don't fully know the logic yet @@ -30,11 +31,11 @@ export const DataCube = ({ volTexture: propVolTexture }: DataCubeProps ) => { },[remapTexture, dataShape]) const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=> new THREE.MeshBasicNodeMaterial(),[useRayMarch, useOrtho, volTexture, colorScale, remapTexture]); - shaderMaterial.colorNode = raymarchClouds() - shaderMaterial.side = THREE.BackSide; - shaderMaterial.transparent = true; - const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); + const shaderMaterial = useMemo(()=> + rayMarchingMaterial() + ,[useRayMarch, useOrtho, volTexture, colorScale, remapTexture]); + + const geometry = useMemo(() => new THREE.BoxGeometry(shape.x, shape.y, shape.z), [shape]); return ( diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index af68fb087..e1540457f 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -1,7 +1,7 @@ "use client"; import React, {useMemo, useEffect, useState, useRef} from 'react' -import * as THREE from 'three' +import * as THREE from 'three/webgpu' import { useAnalysisStore } from '@/GlobalStates/AnalysisStore'; import { useGlobalStore } from '@/GlobalStates/GlobalStore'; import { usePlotStore } from '@/GlobalStates/PlotStore'; @@ -18,6 +18,7 @@ import { useAxisIndices, useDimAxis } from '@/hooks'; import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; import { functionInjector } from '../ui/Elements/ColorAdjuster'; import {InfoViewer} from '../ui/Elements/InfoViewer'; +import { vec4 } from 'three/tsl'; const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[]}) => { // ---- Imports ---- // @@ -177,23 +178,9 @@ const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | TH // ----- SHADER MATERIAL ----- // const uniforms = useCommonUniforms() - const shaderMaterial = useMemo(()=>new THREE.ShaderMaterial({ - glslVersion: THREE.GLSL3, - uniforms:{ - map: {value: textures}, - remapTexture: {value: remapTexture?? remapBorders}, - ...uniforms - }, - defines:{ - ...(isFlat ? { IS_FLAT: true } : {}), - ...(remapTexture ? { REPROJECT: true } : {}) - }, - vertexShader: vertShader, - fragmentShader: functionInjector(flatFrag, colorScale), - side: THREE.DoubleSide, - }),[isFlat, textures, remapTexture, colorScale]) - updateCommonUniforms(shaderMaterial) - + // const shaderMaterial = useMemo(()=> new THREE.MeshBasicNodeMaterial({color: "red"}) + // ,[isFlat, textures, remapTexture, colorScale]) + const shaderMaterial = new THREE.MeshBasicNodeMaterial({color: "red"}) useEffect(()=>{ // This is duplicated. Probably shoud just move it to Plot.tsx useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) diff --git a/src/components/textures/TSL/rayMarcher.ts b/src/components/textures/TSL/rayMarcher.ts index 88df98c60..7e6f5646b 100644 --- a/src/components/textures/TSL/rayMarcher.ts +++ b/src/components/textures/TSL/rayMarcher.ts @@ -1,87 +1,194 @@ -import { Fn, vec4, vec3, time, If, smoothstep, exp, Break, modelWorldMatrixInverse, triNoise3D, pmremTexture, modelWorldMatrix } from 'three/tsl'; -import { RaymarchingBox } from 'three/addons/tsl/utils/Raymarching.js'; - -// Volumetric cloud density calculator (simplified ellipsoid shape with warping and edge noise erosion) -const getCloudDensity = Fn( ( [ p ] : [ any ]) => { - - // 1. Warp coordinate space to deform the geometric boundaries organically (Domain Warping) - const warp = triNoise3D( p, 0.5, time.mul( 0.5 ) ); - const pWarped = p.add( vec3( warp ).sub( 0.5 ).mul( 0.4 ) ); - - // 2. Base Ellipsoid Mask (wider than it is tall to look like a flat cumulus cloud) - const baseMask = smoothstep( 0.48, 0.22, pWarped.mul( vec3( 1, 1.3, 1 ) ).length() ); - - // 3. Volumetric details with slow wind drift - const pNoise = pWarped.add( vec3( time.mul( 0.05 ), 0, time.mul( 0.02 ) ) ); - const noiseVal = triNoise3D( pNoise.mul( 1.2 ), 1.2, time.mul( 0.6 ) ); - - // Erode only the edges of the cloud, keeping the center core solid - const shape = baseMask.sub( noiseVal.mul( baseMask.pow( 2 ).oneMinus().mul( 0.48 ) ) ); - - // Soft threshold to get beautiful rounded boundaries with smooth fade-out - return smoothstep( - 0.02, 0.35, shape ); - -} ); - -export const raymarchClouds = Fn( () => { - - const steps = 48; - - const finalColor = vec4( 0 ); - - // Direct light source (constant direction from top-right-front, matching the reference image) - const lightDir = vec3( 1, 1.2, 0.8 ).normalize(); - const localLightDir = modelWorldMatrixInverse.mul( vec4( lightDir, 0 ) ).xyz.normalize(); - - // Direct light color matching the reference sun light - const directLightColor = vec3( 0.5, 1.0, 1.4 ); - - RaymarchingBox( steps, ( { positionRay, stepSize } ) => { - - const density = getCloudDensity( positionRay ); - - If( density.greaterThan( 0.01 ), () => { - - // Shadow ray: sample density offset towards the light source in local space - const shadowPos = positionRay.add( localLightDir.mul( 0.08 ) ); - const shadowDensity = getCloudDensity( shadowPos ); - - // Dual-Lobe Beer's Law for realistic multiple scattering & light penetration - const shadowVal = shadowDensity.mul( 10 ); - const transmittance = exp( shadowVal.negate() ).mul( 0.5 ).add( exp( shadowVal.mul( 0.1 ).negate() ).mul( 0.65 ) ); - - // Calculate local normal based on positionRay relative to cloud center - const normal = positionRay.normalize(); - - // Transform normal to world space so environment directions align correctly with the sky - const worldNormal = modelWorldMatrix.mul( vec4( normal, 0 ) ).xyz.normalize(); - - // Sample ambient light dynamically in the direction of the world normal from the environment map (IBL) - const ambientLightColor = vec3(0.5); - - // Combine direct light and ambient sky contribution (offset positionRay.y by 0.5 to keep factors positive) - const directLight = directLightColor.mul( transmittance ); - const ambientLight = ambientLightColor.mul( positionRay.y.add( 0.5 ) ).mul( density ); - const cloudColor = directLight.add( ambientLight ); - - // Front-to-back blending with accumulated color (higher opacity for solid volume appearance) - const alpha = density.mul( stepSize ).mul( 8 ); - const colSample = cloudColor.mul( alpha ); - - finalColor.rgb.addAssign( finalColor.a.oneMinus().mul( colSample ) ); - finalColor.a.addAssign( finalColor.a.oneMinus().mul( alpha ) ); - - // Early loop termination if cloud gets opaque - If( finalColor.a.greaterThanEqual( 0.95 ), () => { - - Break(); - - } ); - - } ); - - } ); - - return finalColor; - -} ); +import { Fn, vec4, vec3, abs, If, normalize, pow, EPSILON, Discard, int, ceil, Loop, + min, max, vec2, select, cameraPosition, positionLocal, varying, float, bool, + Break, modelWorldMatrixInverse, modelWorldMatrix, Continue, + texture, mod, clamp, ivec3, fract } from 'three/tsl'; +import * as u from './utils/commonUniforms' +import * as h from './utils/commonHelpers'; +import * as v from './utils/volumeUniforms' +import * as THREE from 'three/webgpu' + +const hitBox = Fn(([orig, dir]: any[]) => { + const boxMax = v.scale.mul(0.5); + const boxMin = boxMax.negate(); + const invDir = vec3(1.0).div(dir); + const tA = boxMin.sub(orig).mul(invDir); + const tB = boxMax.sub(orig).mul(invDir); + const tmin = min(tA, tB); + const tmax = max(tA, tB); + const t0 = max(tmin.x, max(tmin.y, tmin.z)); + const t1 = min(tmax.x, min(tmax.y, tmax.z)); + return vec2(t0, t1); +}); + + +function rayMarch(){ + const vOrigin = varying(modelWorldMatrixInverse.mul(vec4(cameraPosition, 1.0)).xyz, 'vOrigin'); + const vDirection = varying(positionLocal.sub(vOrigin), 'vDirection'); + + return Fn(() => { + const rayDir = normalize(vDirection); + const bounds = hitBox(vOrigin, rayDir).toVar('bounds'); + + If(bounds.x.greaterThan(bounds.y), () => { + Discard(); + }); + + bounds.x.assign(max(bounds.x, 0.0)); + + const inc = vec3(1.0).div(abs(rayDir)); + const delta = min(inc.x, min(inc.y, inc.z)).div(v.steps).toVar('delta'); + + const zStepSize = int(u.textureDepths.y).mul(int(u.textureDepths.x)); + const yStepSize = int(u.textureDepths.x); + const scaler = vec3(1.0).div(v.scale); + + const accumRGB = vec3(0.0).toVar('accumRGB'); + const alphaAcc = float(0.0).toVar('aphaAcc'); + const borderHit = bool(false).toVar('borderHit'); + + const nSteps = int(ceil(bounds.y.sub(bounds.x).div(delta))); + + Loop(nSteps, ({ i }: { i: any }) => { + const t = bounds.x.add(float(i).mul(delta)); + const p = vOrigin.add(rayDir.mul(t)); + + // ---- clip bounds ---- + If( + p.x.greaterThan(v.flatBounds.x.negate()).or(p.x.lessThan(v.flatBounds.y.negate())), + () => { Continue(); }, + ); + If( + p.z.negate().greaterThan(v.flatBounds.z.negate()) + .or(p.z.negate().lessThan(v.flatBounds.w.negate())), + () => { Continue(); }, + ); + If( + p.y.lessThan(v.vertBounds.x).or(p.y.greaterThan(v.vertBounds.y)), + () => { Continue(); }, + ); + + // ---- reprojection + mask ---- + + const rp = h.reprojector(p.mul(scaler).add(0.5)); + If(rp.valid.not(), () => { Continue(); }); + + If(u.maskValue.notEqual(0), () => { + // .level(0): textureSample needs uniform control flow in WGSL, which + // this loop (continue/break/discard) doesn't have. Explicit LOD is fine. + const mask = texture(u.maskTexture, rp.maskUV).r; + const skip = u.maskValue.equal(1).and(mask.lessThan(0.5)) + .or(u.maskValue.notEqual(1).and(mask.greaterThanEqual(0.5))); + If(skip, () => { Continue(); }); + }); + + // ---- texel addressing ---- + const texCoord = vec3(0).toVar('texCoord') + texCoord.xy.assign(rp.texCoord); + texCoord.z.assign(mod(texCoord.z.add(u.animateProg), 1.0001)); + texCoord.assign(clamp(texCoord, vec3(0.0), vec3(1.0).sub(EPSILON))); + + const idx = clamp( + ivec3(texCoord.mul(u.textureDepths)), + ivec3(0), + ivec3(u.textureDepths).sub(ivec3(1)), + ); + const textureIdx = idx.z.mul(zStepSize).add(idx.y.mul(yStepSize)).add(idx.x); + const localCoord = fract(texCoord.mul(u.textureDepths)); + + // ---- sample ---- + const d = float(0.0).toVar('d'); + const biVal = float(0.0).toVar('biVal'); + const biNaN = bool(false).toVar('biNaN'); + + If(u.bivariate, () => { + const bivar = h.sample2ToOrder(localCoord, textureIdx, u.bivariateSelection); + d.assign(bivar.r); + biVal.assign(bivar.g); + biNaN.assign(h.isNaNBits(d).or(h.isNaNBits(biVal))); + }).Else(() => { + d.assign(h.sample1(localCoord, textureIdx)); + d.assign(h.rescaler(d)); + }); + + const isnan = h.isNaNBits(d) + .or(biNaN) + .or(u.useF16.not().and(d.equal(1.0))) + .or(abs(d.sub(u.fillValue)).lessThan(0.005)); + + If(isnan, () => { + const a5 = pow(u.nanAlpha, 5.0); + accumRGB.addAssign(float(1.0).sub(alphaAcc).mul(a5).mul(u.nanColor.rgb)); + alphaAcc.addAssign(a5); + Continue(); + }); + + // (original `else if (!bivariate)`; the Continue above makes a plain If equivalent) + If(u.bivariate.not(), () => { + d.assign(d.mul(u.cScale)); + d.assign(max(min(d.add(u.cOffset), 0.995), 0.0)); + }); + + // ---- threshold + compositing ---- + const inRange = d.greaterThanEqual(u.threshold.x).and(d.lessThanEqual(u.threshold.y)); + + If(inRange, () => { + const col = vec3(0.0).toVar('col'); + If(u.bivariate, () => { + If(u.bivariateSelection.notEqual(0), () => { + col.assign(h.colorMixer(biVal, d)); + }).Else(() => { + col.assign(h.colorMixer(d, biVal)); + }); + }).Else(() => { + col.assign(texture(u.cmap, vec2(d, 0.5)).rgb); + }); + + const alphaFac = select(v.revTransparency, float(1.0).sub(d), d); + const exponent = v.transparency.mul(v.opacityMag); + const alpha = float(0.0).toVar('alpha'); + + If(v.useClipScale, () => { + const normalizedOpacity = clamp( + alphaFac.sub(u.threshold.x).div(u.threshold.y.sub(u.threshold.x)), + 0.0, + 1.0, + ); + alpha.assign(pow(max(normalizedOpacity, 0.001), exponent)); + }).Else(() => { + alpha.assign(pow(max(alphaFac, 0.001), exponent)); + }); + + accumRGB.addAssign(float(1.0).sub(alphaAcc).mul(alpha).mul(col)); + alphaAcc.addAssign(alpha.mul(float(1.0).sub(alphaAcc))); + + If(alphaAcc.greaterThanEqual(1.0), () => { + If(u.useBorderTexture, () => { + const borderDist = texture(u.borderTexture, rp.maskUV).r; + If(borderDist.lessThanEqual(u.borderWidth), () => { + borderHit.assign(bool(true)); + }); + }); + Break(); + }); + }); + }); + const result = vec4(accumRGB, alphaAcc).toVar('result'); + If(borderHit, () => { + result.assign(vec4(u.borderColor, 1.0)); + }); + + If(result.a.equal(0.0), () => { + Discard(); + }); + return result; + })(); +} + +export function rayMarchingMaterial(){ + const material = new THREE.NodeMaterial(); + material.fragmentNode = rayMarch(); + material.side = THREE.BackSide; + material.transparent = true; + material.depthWrite = false; + return material +} \ No newline at end of file diff --git a/src/components/textures/TSL/utils/commonHelpers.ts b/src/components/textures/TSL/utils/commonHelpers.ts index 4404f4f8d..9153b379c 100644 --- a/src/components/textures/TSL/utils/commonHelpers.ts +++ b/src/components/textures/TSL/utils/commonHelpers.ts @@ -1,28 +1,14 @@ import { Fn, min, max, clamp, abs, round, mix, select, texture, floatBitsToUint, uint, fract, float, vec2, vec3, - vec4, If } from 'three/tsl'; -import { valueRange, resolution, bottomLeft, bottomRight, topLeft, - mixMode, lonBounds, latBounds, is360, remapBorders } from './commonUniforms'; - -const IS_FLAT = true; // or false based on your setup - -// Create an array of 12 texture uniform nodes -const maps = Array.from({ length: 12 }, (_, i) => { - const tex = myThreeTexturesArray[i]; // Your THREE.Texture or THREE.Data3DTexture - - return IS_FLAT - ? texture(tex) // Creates 2D sampler uniform node - : texture3D(tex); // Creates 3D sampler uniform node -}); + vec4, If, texture3D } from 'three/tsl'; +import { valueRange, resolution, bottomLeft, bottomRight, topLeft, reproject, + mixMode, lonBounds, latBounds, is360, remapBorders, remapTexture, isFlat, map } from './commonUniforms'; // --- TEXTURE SAMPLERS ---// const sampleMap = Fn(([p, index]: [any, any]) => { const result = vec4(0).toVar(); - maps.forEach((map, i) => { - If(index.equal(i), () => { - result.assign(texture(map, p)); - }); - }); + const tex = map[index]; + result.assign(texture(tex, p)); return result; }); @@ -98,9 +84,8 @@ export const denorm = (x: any) => x.mul(valueRange.y.sub(valueRange.x)).add(valu export const norm = (x: any) => x.sub(valueRange.x).div(valueRange.y.sub(valueRange.x)); export const rescaler = (x: any) => x; -export function reprojector(texCoord: any, opts: Pick) { - const { remapTexture, isFlat, reproject } = opts; - +export function reprojector(texCoord: any) { + let newTexCoord = texCoord; let maskUV: any; let valid: any; @@ -122,14 +107,10 @@ export function reprojector(texCoord: any, opts: Pick texture(new Data3DTexture())) export const maskTexture = texture( new DataTexture() ); export const cmap = texture( new DataTexture() ); export const remapTexture = texture( new DataTexture() ); @@ -30,4 +31,6 @@ export const fillValue = uniform( 'float' ); export const latBounds = uniform( 'vec2' ); export const lonBounds = uniform( 'vec2' ); export const valueRange = uniform( 'vec2' ); -export const useF16 = uniform( 'bool' ); \ No newline at end of file +export const useF16 = uniform( 'bool' ); +export const isFlat = uniform( 'bool' ); +export const reproject = uniform( 'bool' ); \ No newline at end of file diff --git a/src/components/textures/TSL/utils/volumeUniforms.ts b/src/components/textures/TSL/utils/volumeUniforms.ts new file mode 100644 index 000000000..6ecd1791a --- /dev/null +++ b/src/components/textures/TSL/utils/volumeUniforms.ts @@ -0,0 +1,10 @@ +import { uniform } from "three/tsl"; + +export const scale = uniform('vec3'); +export const steps = uniform('int'); +export const flatBounds = uniform('vec4'); +export const vertBounds = uniform('vec2'); +export const transparency = uniform('float'); +export const opacityMag = uniform('float'); +export const useClipScale = uniform('bool'); +export const revTransparency = uniform('bool'); \ No newline at end of file From de7359d7260fb4bca17c211590cf0274ccf6df90 Mon Sep 17 00:00:00 2001 From: Jeran Date: Mon, 5 Oct 2026 15:39:08 +0200 Subject: [PATCH 03/61] Test --- src/components/plots/FlatMap.tsx | 8 +- src/components/plots/Plot.tsx | 27 ++++-- src/components/textures/TSL/flatMap.ts | 89 +++++++++++++++++++ .../textures/TSL/utils/commonHelpers.ts | 11 ++- .../textures/TSL/utils/commonUniforms.ts | 72 +++++++-------- src/hooks/useCommonUniforms.tsx | 64 ++++++++++++- 6 files changed, 218 insertions(+), 53 deletions(-) create mode 100644 src/components/textures/TSL/flatMap.ts diff --git a/src/components/plots/FlatMap.tsx b/src/components/plots/FlatMap.tsx index e1540457f..0cb32b686 100644 --- a/src/components/plots/FlatMap.tsx +++ b/src/components/plots/FlatMap.tsx @@ -18,8 +18,8 @@ import { useAxisIndices, useDimAxis } from '@/hooks'; import { updateCommonUniforms, useCommonUniforms } from '@/hooks/useCommonUniforms'; import { functionInjector } from '../ui/Elements/ColorAdjuster'; import {InfoViewer} from '../ui/Elements/InfoViewer'; -import { vec4 } from 'three/tsl'; - +import { createFlatMapMaterial } from '../textures/TSL/flatMap'; +import { uniformUpdater } from '@/hooks/useCommonUniforms'; const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | THREE.Data3DTexture[]}) => { // ---- Imports ---- // const textures = usePaddedTextures(propTextures); @@ -177,10 +177,10 @@ const FlatMap = ({textures: propTextures} : {textures : THREE.DataTexture[] | TH } // ----- SHADER MATERIAL ----- // - const uniforms = useCommonUniforms() + uniformUpdater(); // const shaderMaterial = useMemo(()=> new THREE.MeshBasicNodeMaterial({color: "red"}) // ,[isFlat, textures, remapTexture, colorScale]) - const shaderMaterial = new THREE.MeshBasicNodeMaterial({color: "red"}) + const shaderMaterial = createFlatMapMaterial(); useEffect(()=>{ // This is duplicated. Probably shoud just move it to Plot.tsx useGlobalStore.setState({timeSeries:{}, dimCoords:{}}) diff --git a/src/components/plots/Plot.tsx b/src/components/plots/Plot.tsx index 56ef76dd2..253366ba9 100644 --- a/src/components/plots/Plot.tsx +++ b/src/components/plots/Plot.tsx @@ -17,7 +17,7 @@ import { useDataFetcher } from '@/hooks/useDataFetcher'; import { reproject } from '@/components/textures/ProjectionTexture'; import { GetCurrentArray } from '@/utils/HelperFuncs'; import { useColormapStore } from '@/GlobalStates/ColormapStore'; - +import {color} from 'three/tsl' const TransectNotice = () =>{ const selectTS = usePlotStore(s => s.selectTS) return ( @@ -28,7 +28,14 @@ const TransectNotice = () =>{ ) } - +function TestMesh() { + const material = new THREE.MeshBasicNodeMaterial({color:'hotpink'}) + return ( + + + + ) +} const Orbiter = ({isFlat} : {isFlat : boolean}) =>{ const {resetCamera, useOrtho, displaceFaces, cameraPosition, overRideCamera} = usePlotStore(useShallow(s => ({ resetCamera: s.resetCamera, useOrtho: s.useOrtho, displaceFaces: s.displaceFaces, @@ -237,18 +244,22 @@ const Plot = () => {