three.js icon indicating copy to clipboard operation
three.js copied to clipboard

[TSL] `instancedArray.element` ignores index parameter with WebGL backend

Open Fennec-hub opened this issue 7 months ago • 7 comments

Description

When using the WebGPURenderer with { forceWebGL: true }, the instancedArray.element() function in Three.js TSL always returns the same value, regardless of the index passed. This breaks dynamic indexing in shaders.

Only occurs with WebGPURenderer using the WebGL backend (i.e., { forceWebGL: true }).

Affected expressions (all return the same result):

  • instancedArray.element(instanceIndex)
  • instancedArray.element(0)
  • instancedArray.element(1)
  • instancedArray.element(int(1))
  • instancedArray.element(float(1))

Expected behavior: Each index should return the corresponding element from the array.

Reproduction steps

  1. Set the WebGPURenderer to { forceWebGL: true }
  2. Create an instancedArray with multiple elements
  3. Use instancedArray.element() with different index parameters in a compute shader
  4. Observe that all calls return the same element regardless of index

Expected: Different indices should return different array elements

Code

const array = instancedArray(1000, "vec3");

Fn(() => {
  const a = array.element(0);
  const b = array.element(1);
  const c = array.element(instanceIndex);
  // All three return the same value in WebGL
});

Live example

This is a modified version of the official WebGPU compute points example, using { forceWebGL: true }. Replacing instanceIndex with any constant still yields the same result:

🔗 https://jsfiddle.net/fzjbs1a6/

Screenshots

No response

Version

r177

Device

Desktop

Browser

Chrome

OS

Linux

Fennec-hub avatar Jun 03 '25 18:06 Fennec-hub

It's necessary set the PBO using like:

const array = instancedArray( 1000, 'vec3' ).setPBO( true );

I will leave the issue open until we improve the checks and add some warnings if necessary. This is only needed for WebGL2 fallback.

sunag avatar Jun 04 '25 04:06 sunag

I can confirm that it is indeed working with .setPBO(true). jsFiddle.

Thank you so much, @sunag, for the clarification, the prompt response, and your amazing work on TSL.

P.S. As far as I'm concerned, the issue is resolved. Feel free to close it at your discretion.

Fennec-hub avatar Jun 04 '25 05:06 Fennec-hub

@sunag the same true for attributeArray? In example here attributeArray.element(i) does not function correctly making the birds converge in the center instead of exhibiting boid behavoiur with WebGL2, even though setPBO(true) is used. Replacing it with instancedArray works fine.

LastPhoen1x avatar Jun 04 '25 09:06 LastPhoen1x

@Fennec-hub Oh, thank you for your words :)

@LastPhoen1x I noticed this problem earlier but didn't realize it was an attributeArray, which makes a lot of sense, i will fix it according to the logic of the example. It would be another important point to notify the developer if used incorrectly.

sunag avatar Jun 05 '25 16:06 sunag

@sunag I hate to bother you again, but does instancedArray.element(i).assign() only able to assign the value at current index due to some technical limitation of workgroups? It seems to ignore the index even with setPBO() on (https://jsfiddle.net/1cLhoaw4/5/)

LastPhoen1x avatar Jun 07 '25 13:06 LastPhoen1x

@sunag I hate to bother you again, but does instancedArray.element(i).assign() only able to assign the value at current index due to some technical limitation of workgroups? It seems to ignore the index even with setPBO() on (https://jsfiddle.net/1cLhoaw4/5/)

Hi @LastPhoen1x. I'm not exactly sure how to resolve this issue, but I can explain what is going on under the hood.

When using the WebGL Fallback renderer, what would be a storage buffer in WebGPU gets represented as a nodeVarying element in the 'compute' shader (which is just a vertex shader that has been retrofitted to be used for compute purposes)

This is the 'compute' code generated by the GLSLNodeBuilder

#version 300 es

// Three.js r178dev - Node System

// Bunch of extra stuff

// uniforms
uniform highp sampler2D nodeUniform0;

// varyings
out vec3 nodeVarying0;

// attributes
layout( location = 0 ) in vec3 nodeAttribute1;

// codes
void main() {

	// vars
	// transforms
	nodeVarying0 = nodeAttribute1;

	// flow
	// code

	if ( ( float( uint( gl_InstanceID ) ) == 0.0 ) ) {

		nodeVarying0 = vec3( 1.0, 1.0, 1.0 );
		
	}

	gl_PointSize = 1.0;
}

Unfortunately, the value of nodeVarying0 is always going to represent the value of posArray at index gl_InstanceID. Accordingly, if you were to change the code to this:

const changeOnlySecondElement = Fn( () => {
			
	If( instanceIndex.equal( 1 ), () => { //ONLY if instanceIndex is 0
		posArray.element( 423423432 ).assign( vec3( 1, 1, 1 ) ); //assign to element 1 only
	} );

Then the correct element of the buffer would be written over, since, in the second iteration of the compute function, gl_InstanceID would equal 1 and nodeVarying0 would also represent posArray at index 1. As seen above, it's not even accounting for or reading whatever value gets put in element.

Presumably there's some way that the renderer is moving data from this varying into the dataTexture that holds the pbo but I'm not sure how that is done.

On the other hand, if you try to set the posArray elements as vars, it accesses each element of the array as separate variables.

// TSL CODE

const firstElement = posArray.element( 0 ).toVar( 'test1' );
const secondElement = posArray.element( 1 ).toVar( 'test2' );

// GLSL Output

// vars
vec3 nodeVar0;
uint nodeVar0Size;
vec3 test1;
vec3 nodeVar1;
vec3 test2;
// transforms

// flow
// code
nodeVar0Size = uint( textureSize( nodeUniform0, 0 ).x );
nodeVar0 = vec4(texelFetch( nodeUniform0, ivec2(0u % nodeVar0Size, 0u / nodeVar0Size), 0 )).xyz;
test1 = nodeVar0;
nodeVar1 = vec4(texelFetch( nodeUniform0, ivec2(1u % nodeVar0Size, 1u / nodeVar0Size), 0 )).xyz;
test2 = nodeVar1;

There are some shader extensions for OpenGL 4.2 (EXT_shader_image_load_store) that could potentially help circumvent this issue but I haven't searched to see if this is available in WebGL2.

The renderer could also align more closely with GPUComputationRenderer and move to a fragment shader approach for compute shaders, which would help with backporting old WebGLRenderer compute shaders and also be more intuitive than the varying approach.

cmhhelgeson avatar Jun 08 '25 18:06 cmhhelgeson

I believe in /src/renderers/webgl-fallback/nodes/GLSLNodeBuilder.js

generatePBO( storageArrayElementNode ) {
	const { node, indexNode } = storageArrayElementNode;
	...
	const indexSnippet = indexNode.build( this, 'uint' );

which passes "this" to src/nodes/core/IndexNode.js

generate( builder ) {
	...
	} else if ( scope === IndexNode.INSTANCE ) {
		propertyName = builder.getInstanceIndex();

And that getInstanceIndex() grabs the index of the current thread, not sure what it should pass to get the correct index. I'll mess with it later unless someone knows an easy fix.

LastPhoen1x avatar Jun 09 '25 19:06 LastPhoen1x