How to port vMapUv to TSL?

Here’s your example updated with my TSL, unable to get clipping to work on the left edge of the texture map: https://codepen.io/trusktr/pen/WbxbBdK

The old-style patched-GLSL version works exactly as expected: https://codepen.io/trusktr/pen/EayazQx

Does this provide any more clues? Is there a TSL bug, or are we missing something?

Absolutely. By experimenting I’ve found that uv() returns the unaffected UVs. Try this:

const mapUv = uv().add(map.offset)
1 Like

That indeed makes it do the thing! Thank you! https://codepen.io/trusktr/pen/OPXVJeK

With this I think I can move the project onto WebGPU finally. :partying_face:

So how exactly do we get the already transformed uv? Because we also have to account for all four map.offset, map.repeat, map.rotation, and map.center.

Looking at TextureNode docs, it looks like there is a separate uv node per texture. Maybe we’re supposed to get the map-specific uv node instead of using a default uv()?

I see texture() seems to return the texture-specific uv. How exactly do we get the one for the .map?

1 Like

It’s not quite as straight forward as simply accessing the vMapUv in a legacy GLSL shader patch. I think the solution requires setting up the texture node manually (replacing the built in node); more cumbersome, but perhaps more flexible. (@sunag could we perhaps expose global texture uvs so we don’t have to override texture nodes? Or does that exist already and we missed it?)

I asked Google AI Mode

how do we read the transformed uv of a texture in threejs TSL with webgpurenderer? when I use the global uv() node, it is a non-transformed node, but I need the transformed uv, similar to the vMapUv equivalent when patching GLSL shaders, to derive further logic based in the transformed uv.

and it replied

Google AI Mode's response

In TSL (Three.js Shading Language), the standard way to access the transformed UV coordinates for a specific texture is by using the texture’s method, not the global node directly with the material’s properties. [1, 2]

When a is used as a on a material (e.g., ), the texture’s offset, repeat, and rotation properties are automatically applied to its corresponding UV transform. To access this transformed UV within a custom node network, you use the specific texture node to derive the UV. [3, 4]

Accessing Transformed UV

Instead of using the global node (which returns the base, untransformed UV attribute), you should use the method of the node you are working with. [5]

Here is how you would implement this in TSL:

import * as THREE from 'three/webgpu';
import { texture, uv } from 'three/tsl';

// Assume 'colorMap' is a THREE.Texture instance with offset/repeat/rotation set
const colorMap = new THREE.TextureLoader().load( 'path/to/your/texture.jpg' );
// Apply some transformation (for example)
colorMap.repeat.set( 2, 2 );
colorMap.offset.set( 0.5, 0 );

// 1. Create a texture node
const colorMapNode = texture( colorMap );

// 2. Access the transformed UV using the .uv() method of the texture node
// This automatically incorporates the texture's matrix transformations
const transformedUV = colorMapNode.uv(); 

// You can now use 'transformedUV' for further calculations
// For example, sampling another texture with the same transformed UV
const detailMap = new THREE.TextureLoader().load( 'path/to/your/detailmap.jpg' );
const detailNode = texture( detailMap, transformedUV );

// Use the detail in the material
const material = new THREE.MeshStandardNodeMaterial();
material.colorNode = colorMapNode.mul( detailNode );

// Add the material to a mesh and scene...

The key is that the expression (or ) is what generates the TSL equivalent of the varying in standard GLSL shaders. It correctly applies the texture’s internal matrix. [1, 6]

AI responses may include mistakes.

[1] TSL: Missing a uvNode to rewrite the UV's · Issue #30160 · mrdoob/three.js · GitHub

[2] https://www.youtube.com/watch?v=vLz2Rk1r_gQ

[3] Texture - Three.js Docs

[4] https://www.reddit.com/r/blenderhelp/comments/15mf25c/pack_uvs_correctly/

[5] https://catlikecoding.com/unity/tutorials/advanced-rendering/triplanar-mapping/

[6] https://stackoverflow.com/questions/52176463/threejs-how-to-render-a-texture-with-custom-shaders-including-the-textures-uv-o

So, here’s my attempt, but it doesn’t clip anything, just like before: https://codepen.io/trusktr/pen/bNeddzx

How do we get the fully transformed uv like legacy vMapUv?

I don’t know whether there is a node or a function that returns the transformed UV.

The current code uv().add(map.offset) only adds offset. For a full version it should do all 5 steps, not only the last one:

  • subtract center with sub *
  • multiply repeat with mul *
  • rotate by angle with rotate
  • add center with add
  • add offset with add

* I’m not sure whether the order is sub-mul or mul-sub, both are valid, only the result is different, I have never tried which one is used in Three.js. If it is mul-sub, then the middle 3 operations can be done with rotateUV

Edit: For the JS Matrix3 there is setUvTransform - maybe if you implement it in TSL it would be enough?

Texture already has .matrix, filled with .setUvTransform in .updateMatrix().
So, to have the equivalent of vMapUv, maybe, it’s enough to do something like this:

const vMapUv = tsl.mat3(your_texture.matrix).mul(uv());

Haven’t tested. Just a thought.

2 Likes

Ah. Came back from sleep just to write this… to only find you already posted it…

Edit: Tried it. Besides map.updateMatrix() it needs homogeneous coordinates, so the extra 1 does this:

const vMapUv = mat3(map.matrix).mul(vec3(uv(),1)) 
2 Likes

Thanks y’all! Yeah, recreating the same thing Threejs is already doing *should * work. Aren’t we creating new shader inputs that way though? Or at best additional calculations that the shader is already doing elsewhere?

It seems like we should just have access to these sorts of common already-calculated variables, similar to materialOpacity etc. If that’s not already the case, maybe it’s only a small contribution away!

1 Like

@prisoner849 @PavelBoytchev The mat3 trick isn’t quite working.

Here’s the old-style GLSL patch for reference: https://codepen.io/trusktr/pen/EayazQx

And here’s the TSL version with mat3(map.matrix) * vec3(uv): https://codepen.io/trusktr/pen/pvbJLJm

What I see is that some faces aren’t clipped, unlike the GLSL version where all faces are clipped (expected).

Side by side, GLSL first:

Another issue is animating map.offset works with WebGLRenderer as expected:

https://codepen.io/trusktr/pen/KwMpopY

but not with WebGPURenderer and this mat3 trick:

https://codepen.io/trusktr/pen/MYewVjg

GLSL video:

TSL video:

It seems the nodes are not updated with the latest map.matrix automatically, so we need to additionally think about how to update the nodes from the external inputs. It also highlights how the custom TSL variable is separate from the underlying actual uv variable that is somewhere in the shader.

Accessing the actual map uv seems like the proper way to go, as with the GLSL example.

Putting the map.matrix in a uniform() node makes it all work, with and without animation:

No animation: https://codepen.io/trusktr/pen/vEKORrr

Animated: https://codepen.io/trusktr/pen/VYjLXBX

Without the uniform(), it seems there’s something about the timing that captures the map.matrix value at shader compile time with and without the matrix being updated (for each cube face? Hmm).

@Mugen87 Did we miss something, or are there some things we could improve? I’d like to make some contributions:

  • Provide access to existing uv variables (f.e. the equivalent of vMapUv in GLSL), if that doesn’t already exist in TSL (did we miss it?), to avoid re-creating what the shader already does.
  • ensure that nodes that receive values such as matrices and vectors will be up to date when those inputs change, without specifically requiring use of uniform() to make things work as expected (I assume inputs are uniforms already anyway). F.e. mat3(map.matrix) can be updated automagically.

Does this one work?

https://codepen.io/boytchev/pen/qENdoGx?editors=0010

Declared the matrix as uniform, so it is sent down to GPU:

var uvmat = uniform(map.matrix);

var opacityNode = Fn(() => {
   const mapUv = mat3(uvmat).mul(vec3(uv(),1))
    :
})();

Edit: you already found the uniform trick by yourself.

hehe yeah, that’s what I found out in my last comment. Putting it in a uniform() node works (and then mat3 is no longer required, so just uvmat.mul(vec3(uv(),1)) works).

Something about TSL makes me feel like it could/shouhld just work no matter which node is receiving the input. Maybe that’s too costly though? Maybe without uniform() it is creating a literal inlined mat3 (I haven’t checked) which may be more desirable for things that shouldn’t change?

A shorter way without additional variable:

const mapUv = uniform(map.matrix).mul(vec3(uv(), 1))

The pure value of map.matrix is not a node, so its current value is embedded as constant in the code (see mat3 below). If there is a node for the uv matrix it could be used directly, instead of uniforming it.

nodeVar0 = texture( nodeUniform1, ( f_nodeUniform2 * vec3( nodeVarying5, 1.0 ) ).xy );
DiffuseColor = ( vec4( f_nodeUniform0, 1.0 ) * nodeVar0 );

if ( ( ( f_nodeUniform3 * smoothstep( ( - ( fwidth( ( mat3( 1.0, 0.0, 0.0, 0.0,
1.0, 0.0, -0.673073707264649, 0.0, 1.0 ) * vec3( nodeVarying5, 1.0 ) ).x ) * 1.5
) ), 0.0, ( mat3( 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -0.673073707264649, 0.0, 1.0 ) *
vec3( nodeVarying5, 1.0 ) ).x ) ) <= 0.0 ) ) {

    discard;
    discard;
    

}
2 Likes

That begs the question, why was the static mode mat3(map.matrix) incorrect for some of the cube sides? Why was it only correct when using uniform(map.matrix)?

Because the matrix was still not updated. In the static version if you call map.updateMatrix() right after modifying the offset property, the matrix is calculated and used even without uniform.

1 Like

Something still seems off about that: why would it have been updated for part of the mesh geometry, and not the whole mesh? I’d expect it to be correct, or incorrect, for all faces (f.e. either it updated before applying to the mesh, or it didn’t).

The only thing I could think of that could cause this is if parts of the mesh geometry are being processed async (f.e. like await statements between groups of vertices). I doubt (though it could be possible) that Three.js would introduce async complexity like that, that runs update logic mid-way during geometry processing, judging from how much threejs historically values simplicity (I also love the simplicity).