mrdoob / three.js

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

WebGPURenderer: wrong update of compressed textures #29786

Open vlucendo opened 1 week ago

vlucendo commented 1 week ago

Description

Updating compressed textures with the contents of others of different formats by copying them fails sometimes and have visual errors, but the same functionality works fine in WebGL. This functionality is often used when creating an empty texture while the asset loads, and then copying the loaded data to it once it's available.

I created a couple of fiddles with webgpu and webgl to compare. Webgpu only updates 3 times instead of 4 and displays a wrong texture.

Reproduction steps

(see fiddles)

Code

See fiddles

Live example

Screenshots

No response

Version

r170

Device

Desktop

Browser

Chrome

OS

Windows

vlucendo commented 1 week ago

I updated the links to show an example of textures displaying wrong as well.

GitHubDragonFly commented 3 days ago

Just a minor addition, support for one of the texture formats also appears to be missing.

This could be tested by using the fiddle and changing the initial texture from 0 to 1 and then 2 and then 3 (this last one should show the error related to Unsupported format 1023).

RenaudRohlinger commented 2 days ago

The compression format is properly supported and works as expected when the texture is initialized, for example:

const textureNode = THREE.texture(textures[3]);

However, the issue seems to be that the WebGPURenderer or the NodeBuilder doesn't yet handle dynamic reassignment of the compressedTexture format.

GitHubDragonFly commented 2 days ago

@RenaudRohlinger I am not an expert for these things but that texture seems to be causing some issues.

You and @vlucendo could try the following code in the WebGPU fiddle, which is using a different texture and is also disposing of each texture. It seems to work fine for switching all 4 textures but I am not sure how accurate it is.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'

let mesh, renderer, scene, camera;

init();

async function init() {

    // renderer
    renderer = new THREE.WebGPURenderer();
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.setPixelRatio( window.devicePixelRatio );
    document.body.appendChild( renderer.domElement );

    // wait for renderer
    await renderer.init();

    // scene and camera
    scene = new THREE.Scene();
    scene.background = new THREE.Color( 0x000000 );
    camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 10000 );
    camera.position.set( 0, 0, 20 );

    // mesh
    const geometry = new THREE.SphereGeometry( 5, 64, 32 );
    const material = new THREE.MeshBasicNodeMaterial();
    mesh = new THREE.Mesh( geometry, material );
    scene.add( mesh );

    // bug report

    // create loader
    const loader = new KTX2Loader();
    loader.detectSupport(renderer);
    loader.setTranscoderPath('https://unpkg.com/three/examples/jsm/libs/basis/');

    // load texture
    const textures = await Promise.all([
        loader.loadAsync('https://threejs.org/examples/textures/compressed/2d_uastc.ktx2'),
        loader.loadAsync('https://threejs.org/examples/textures/compressed/2d_etc1s.ktx2'),
        loader.loadAsync('https://threejs.org/examples/textures/compressed/sample_uastc_zstd.ktx2'),
        loader.loadAsync('https://threejs.org/examples/textures/compressed/sample_etc1s.ktx2'),
    ]);

    const compressedTexture = new THREE.CompressedTexture();
    const textureNode = THREE.texture(compressedTexture);

    // create color node
    const getWGSLTextureSample = THREE.wgslFn( `
                    fn getWGSLTextureSample( tex: texture_2d<f32>, tex_sampler: sampler, uv:vec2<f32> ) -> vec4<f32> {
                        return textureSample( tex, tex_sampler, vec2f(uv.x, 1.0 - uv.y) );
                    }
        `);
    material.colorNode = getWGSLTextureSample( { tex: textureNode, tex_sampler: textureNode, uv: THREE.uv() } );

        // assign first texture before render. if the texture on index 2 is selected first, none updates
    compressedTexture.copy(textures[0]);

        // change texture periodically: it should change 4 times but only changes 3 and has visual errors
        let textureIndex = 0;
        setInterval(() => {
            textureIndex = (textureIndex + 1) % textures.length;
            compressedTexture.dispose();
            compressedTexture.copy(textures[textureIndex]);
        compressedTexture.needsUpdate = true;
    }, 500);

    // start animation
     renderer.setAnimationLoop( animate );
}

function animate() {

    renderer.render( scene, camera );

}
GitHubDragonFly commented 1 day ago

Just to mention that, with respect to the current code of the KTX2 Loader, this might not really be an issue but just a wrong approach to treating all returned textures as THREE.CompressedTexture.

Even though the documentation suggests that the load method would return THREE.CompressedTexture, the loader's code suggests that it could also be THREE.DataTexture or THREE.Data3DTexture - logging all loaded textures from the original WebGPU fiddle shows that the 4th texture was loaded with isDataTexture instead of with isCompressedTexture.

The WebGPU fiddle's code seems to specifically treat them all as THREE.CompressedTexture:

    const compressedTexture = new THREE.CompressedTexture();
    const textureNode = THREE.texture(compressedTexture);

As stated before, I am not an expert for these things and will leave it to the experts to figure it out.

vlucendo commented 12 hours ago

KTX2Loader was returning a DataTexture for the last texture, so I replaced it so they are all CompressedTextures on the following updated fiddles:

WebGPU WebGL

The WebGPU version still seems to have issues, updating 2 times instead of 4. The WebGL version always updates, but also seems to display an error with the new texture now.

Doing .dispose() on the used compressed texture before new data is being copied to it indeed seems to fix the unexpected behaviour, however one would expect that both WebGL and WebGPU worked similarly by just copying the new data and doing needsUpdate = true in my opinion.

Some loaders in core (TextureLoader or CompressedTextureLoader for example) seem to expect this: they return and empty texture and when the data is loaded, they copy it and flag needsUpdate on the previously returned texture.

GitHubDragonFly commented 3 hours ago

As a wild guess, maybe those could be 2 things that require correction: