mrdoob / three.js

JavaScript 3D Library.
https://threejs.org/
MIT License
102.9k stars 35.39k forks source link

Support non-integer loop start and ends when using pragma unroll_loop_* #28020

Open sguimmara opened 8 months ago

sguimmara commented 8 months ago

Description

Currently, the pragma only supports integer values in its regexp:

/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g

This makes it impossible to unroll loops with statically defined values, such as:

#define TEXTURE_COUNT 3

sampler2D textures[TEXTURE_COUNT];

#pragma unroll_loop_start
for ( int i = 0; i < TEXTURE_COUNT; i++ ) {
   vec4 color = texture2D(textures[i], vec2(0, 0));
}
#pragma unroll_loop_end

This would be useful to access samplers in an array, as GLSL forbids the user of dynamic indices to access samplers.

In my situation, I cannot use texture arrays as the various textures in the sampler array do not have the same sizes.

Solution

Currently, in WebGLProgram.js, the unrolling is done right after various preprocessing, such as includes:

vertexShader = resolveIncludes( vertexShader );
vertexShader = replaceLightNums( vertexShader, parameters );
vertexShader = replaceClippingPlaneNums( vertexShader, parameters );

fragmentShader = resolveIncludes( fragmentShader );
fragmentShader = replaceLightNums( fragmentShader, parameters );
fragmentShader = replaceClippingPlaneNums( fragmentShader, parameters );

vertexShader = unrollLoops( vertexShader );
fragmentShader = unrollLoops( fragmentShader );

We could make unrollLoops aware of the defines in the material, and if the loop end/start are identifiers rather than integers, lookup the indentifiers in the defines of the material to access the actual value:

vertexShader = unrollLoops( vertexShader , defines );
fragmentShader = unrollLoops( fragmentShader, defines );

Then, we slightly modify loopReplace to capture the defines parameter:

function unrollLoops( string, defines ) {

    function loopReplacer( match, start, end, snippet ) {

        let string = '';

        const loopStart = defines[ start ] ?? parseInt( start );
        const loopEnd = defines [ end ] ??  parseInt( end );

        for ( let i = loopStart; i < loopEnd; i ++ ) {

            string += snippet
                .replace( /\[\s*i\s*\]/g, '[ ' + i + ' ]' )
                .replace( /UNROLLED_LOOP_INDEX/g, i );

        }

        return string;

    }

    return string.replace( unrollLoopPattern, loopReplacer );

}

Alternatives

I also considered adding hooks to the preprocessing of the shader (à la onBeforeCompile), so that the user could perform the replacement of the loop-related defines by actual integers:

but it feels a bit too heavy.

Additional context

No response

gkjohnson commented 8 months ago

This has been discussed previously in #20305

sguimmara commented 8 months ago

@gkjohnson My bad I missed this PR. I guess then that my proposal is refused as it complexifies the unroling mechanism.

Since we don't want to complexify the unrolling mechanism, the only way I see is by providing a hook that is executed just before the unrolling, and that let the user change the shader code directly.

gkjohnson commented 8 months ago

the only way I see is by providing a hook that is executed just before the unrolling, and that let the user change the shader code directly.

As mentioned in the other issue, this can be done with the onBeforeCompile callback.

sguimmara commented 8 months ago

@gkjohnson Unfortunately, this does not work if the unrollable loop is in an included chunk. It does work if I inline the chunk though.

mrdoob commented 8 months ago

Are these unroll loops still necessary? They sure made a difference on iPads 10 years ago but I wonder what the is situation now.

mrdoob commented 8 months ago

This would be useful to access samplers in an array, as GLSL forbids the user of dynamic indices to access samplers.

Oh, I see...

mrdoob commented 8 months ago

@sguimmara Have you tried using the new WebGPURenderer? It comes with a WebGL fallback.

sguimmara commented 8 months ago

@mrdoob yes, the dynamic index limitation of WebGL is extremely irritating and prevents a lot of cool features. For example I have to recompile the shader every time the number of iterations in the loop changes (luckily it does not change too often).

Have you tried using the new WebGPURenderer? It comes with a WebGL fallback.

No, but I'm very interested in WebGPU as it might help with performance.

For info, we use three.js to develop Giro3D, a visualization library for geospatial data. This library is all about data streaming, dynamic updates, GPU picking, and WebGL shows its age for this use case.