Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

USDZ exporter premultiplies alpha in textures when Texture.premultiplyAlpha == false #30040

Open
Mirei3D opened this issue Dec 5, 2024 · 1 comment
Labels

Comments

@Mirei3D
Copy link

Mirei3D commented Dec 5, 2024

Description

When exporting textures the Canvas 2D API is used to convert Three textures into PNGs. But while input textures usually do not use premultiplied alpha, the output bitmap of a CanvasRenderingContext2D must always use premultiplied alpha for transparent colors. This implies a lossy conversion step when drawing a non-premultiplied texture to a Canvas 2D rendering context where invisible colors disappear.

I encountered this bug in the USDZ exporter but I suspect that other exporters (like the GLTF exporter) have the same bug.

Reproduction steps

  1. Run the live example
  2. Open the automatically downloaded USDZ in macOS Preview or iOS QuickLook
  3. Check for this defect on the checkmark.

Code

// code goes here

Live example

https://jsfiddle.net/ymtn56hv/

Screenshots

Bildschirmfoto 2024-12-05 um 13 09 33

Version

r171

Device

Desktop

Browser

Chrome

OS

MacOS

@Mirei3D Mirei3D changed the title USDZ exporter applies premultiplies alpha in textures when Texture.premultiplyAlpha == false USDZ exporter premultiplies alpha in textures when Texture.premultiplyAlpha == false Dec 5, 2024
@Mirei3D
Copy link
Author

Mirei3D commented Dec 5, 2024

I have been able to fix the issue locally by extracting texture data via a WebGL 2 context instead and using the fast-png library to encode to PNG. But it comes at a heavy performance cost, which I imagine would be even worse for non-trivial scenes, especially when converting to USDZ for iOS AR.

Do note though that this is unfinished and disregards flipY and premultipliedAlpha and doesn't implement maxTextureSize properly. As it was so slow already, I didn't bother implementing them properly, but it should be possible.

import {
    encode
} from 'fast-png';

const _gl = new OffscreenCanvas(1, 1).getContext('webgl2');

function imageToPng( image, flipY, maxTextureSize ) {

    if ( ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
        ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
        ( typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas ) ||
        ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap ) ) {

        // TODO: implement flipY, premultipliedAlpha, implement maxTextureSize properly

        let data;

        const width = image.width;
        const height = image.height;

        const glTexture = _gl.createTexture();
        const glFramebuffer = _gl.createFramebuffer();
        _gl.bindTexture( _gl.TEXTURE_2D, glTexture );
        _gl.texImage2D( _gl.TEXTURE_2D, 0, _gl.RGBA, _gl.RGBA, _gl.UNSIGNED_BYTE, image );
        _gl.bindFramebuffer( _gl.FRAMEBUFFER, glFramebuffer );
        _gl.framebufferTexture2D( _gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, glTexture, 0 );

        if ( width > maxTextureSize || height > maxTextureSize ) {

            data = new Uint8Array( 4 * maxTextureSize * maxTextureSize ); // TODO: fix texture size

            const glTextureDownScale = _gl.createTexture();
            const glFramebufferDownScale = _gl.createFramebuffer();

            _gl.bindTexture( _gl.TEXTURE_2D, glTextureDownScale );
            _gl.texStorage2D( _gl.TEXTURE_2D, 1, _gl.RGBA8, maxTextureSize, maxTextureSize ); // TODO: fix texture size
            _gl.bindFramebuffer( _gl.DRAW_FRAMEBUFFER, glFramebufferDownScale );
            _gl.framebufferTexture2D( _gl.DRAW_FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, glTextureDownScale, 0 );
            _gl.bindFramebuffer(_gl.READ_FRAMEBUFFER, glFramebuffer);
            _gl.blitFramebuffer(0, 0, width, height, 0, 0, maxTextureSize, maxTextureSize, _gl.COLOR_BUFFER_BIT, _gl.LINEAR); // TODO: fix texture size

            _gl.bindFramebuffer( _gl.FRAMEBUFFER, glFramebufferDownScale );
            _gl.readPixels( 0, 0, maxTextureSize, maxTextureSize, _gl.RGBA, _gl.UNSIGNED_BYTE, data ); // TODO: fix texture size

            _gl.deleteFramebuffer( glFramebufferDownScale );
            _gl.deleteTexture( glTextureDownScale );

            return encode( { data: data, width: maxTextureSize, height: maxTextureSize } ); // FIXME! this leaks glFramebuffer, glTexture

        } else {

            data = new Uint8Array( 4 * width * height );

            _gl.readPixels( 0, 0, width, height, _gl.RGBA, _gl.UNSIGNED_BYTE, data );

        }

        _gl.deleteFramebuffer( glFramebuffer );
        _gl.deleteTexture( glTexture );

        return encode( { data: data, width: width, height: height } );

    } else {

		throw new Error( 'THREE.USDZExporter: No valid image data found. Unable to process texture.' );

	}

}

@Mugen87 Mugen87 added the Addons label Dec 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants