Using data textures in a TSL shader to generate both vertex and fragment. UV issue

Hello everyone, I have a problem that I’ve been staring at soooo long. I hope some of you kind souls can help. Any pointers would be massively appreciated.

I’m generating data textures for height values and color values.
The incoming data looks like: [2,3,5,6,0,9] for example. The height at a given point is just the sum of these values.

These are saved one dataTexture per array item. So the above would have 6 color textures in the userData part of the PlaneGeometry.

The height generation works fine.

But the colors are a mess. I’m pretty certain it’s something to do with UVs, but what I just can’t say.

To keep it simple I’m just generating two spikes on the Plane.
The corner spike kind of works, but the one in the middle looks like maybe it’s wrapping around so the final color is starting again from the bottom?

Maybe the problem with my code will be obvious to you. I REALLY hope so!

Thanks in advance

B

```

import { varying,cross,vec2,texture,float,floor, clamp,mx_noise_float, distance, exp,step, vec3, color, uniform, mix, uv, positionLocal, attribute,Fn,transformNormalToView } from ‘three/tsl’;

import { MeshSSSNodeMaterial, DoubleSide, Texture, MeshStandardNodeMaterial  } from ‘three/webgpu’;

const COLLECTION_COLORS =  [

‘#FF6B44’,  // Hot Pink

‘#00D9ff’,  // Electric Cyan

‘#FFD93D’,  // Bright Yellow

‘#B84FFF’,  // Vivid Purple

‘#959392’,  // Deep Orange

];

/**

g here is the geometry to be applied to

*/export const getMat = (g, hoverUV) => {

// … your validation code …

if (!g.userData.heightTexture || !g.userData.timelineTextures) {

console.error(‘❌ Missing texture data!’);

return new MeshSSSNodeMaterial({ color: 0xff0000, side: DoubleSide });

    }

console.log(‘🎨 Creating material for’, g.userData.numTimelines, ‘on g:’, g);

const redMat = new MeshStandardNodeMaterial({

roughness: 0.4,

metalness: 0.5,

side: DoubleSide,

transparent: false,

});

const origin = COLLECTION_COLORS;

const colors = origin.map(hex => uniform(color(hex)));

//console.log(‘origin:’, origin);

const vNormal = varying(vec3());

const vWorldZ = varying(float());

const vUV = varying(vec2());

const vHeight = varying(float());

const minHeight = uniform(g.userData.minHeight);

const maxHeight = uniform(g.userData.maxHeight);

//console.log("minmax: ", g.userData.minHeight, g.userData.maxHeight)

// Texture nodes

const heightTexNode = texture(g.userData.heightTexture);

const timelineTexNodes = g.userData.timelineTextures.map(tex => texture(tex));

const gridSize = uniform(g.userData.gridSize);

const scale = uniform(1.0);

// Add hover UV uniform - make it accessible for updates

const hoverUVUniform = uniform(vec2(

hoverUV?.x ?? -1.0,

hoverUV?.y ?? -1.0

));

// Store reference to the uniform for later updates

redMat.userData.hoverUVUniform = hoverUVUniform;

//const numTimelines = g.userData.numTimelines;

const numTimelines = g.userData.numTimelines -1;

// VERTEX SHADER

redMat.positionNode = Fn(() => {

const position = positionLocal.xyz.toVar();

const gridUV = uv();

// Sample total height and displace

const height = heightTexNode.sample(gridUV).r;

position.z.assign(height.mul(scale));

// Pass to fragment shader

vWorldZ.assign(position.z);

vUV.assign(gridUV);

vHeight.assign(height);

// Calculate normals

const offset = float(1.0).div(gridSize);

const heightRight = heightTexNode.sample(gridUV.add(vec2(offset, 0.0))).r;

const heightUp = heightTexNode.sample(gridUV.add(vec2(0.0, offset))).r;

const posRight = vec3(position.x.add(1.0), position.y, heightRight.mul(scale));

const posUp = vec3(position.x, position.y.add(1.0), heightUp.mul(scale));

const edgeRight = posRight.sub(position);

const edgeUp = posUp.sub(position);

const normal = cross(edgeRight, edgeUp).normalize();

vNormal.assign(normal);

return position;

})();

redMat.normalNode = transformNormalToView(vNormal);

redMat.colorNode = Fn(() => {

// Sample all timelines (these are LINEAR values)

const timelineValues = [];

let totalLinear = float(0.0).toVar();

let sum = float(0.0);

let tlTotes = [];

for (let i = 0; i < numTimelines; i++) {

const val = timelineTexNodes[i].sample(vUV).r;

sum = sum.add(float(val));

tlTotes.push(sum);

timelineValues.push(float(val));

}

totalLinear.assign(float(tlTotes[tlTotes.length-1]));

const linearHeight = vHeight;

// Now calculate position as linear proportion

const heightRatio = linearHeight.div(totalLinear);

// Calculate bands in LINEAR space

let colorIndex = float(0.0).toVar();

let cumulativeProportion = float(0.0).toVar();

for (let i = 0; i < numTimelines; i++) {

const contribution = timelineValues[i].div(totalLinear.add(0.001));

cumulativeProportion.assign(cumulativeProportion.add(contribution));

colorIndex.assign(colorIndex.add(step(cumulativeProportion, heightRatio)));

}

// Use the working color loop

let finalColor = colors[0].toVar();

for (let i = 1; i < numTimelines; i++) {

finalColor.assign(mix(finalColor, colors[i], step(float(i), colorIndex)));

}

return finalColor;

})();

return redMat;

};