Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions docs/api/en/extras/TextureUtils.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="../../../" />
<script src="page.js"></script>
<link type="text/css" rel="stylesheet" href="page.css" />
</head>
<body>
<h1>[name]</h1>

<p class="desc">A class containing utility functions for textures.</p>

<h2>Methods</h2>

<h3>[method:Number getByteLength]( [param:Number width], [param:Number height], [param:Number format], [param:Number type] )</h3>
<p>
Given the width, height, format, and type of a texture. Determines how
many bytes must be used to represent the texture.
</p>

<h2>Source</h2>

<p>
[link:https://github.com/mrdoob/three.js/blob/master/src/[path].js src/[path].js]
</p>
</body>
</html>
1 change: 1 addition & 0 deletions src/Three.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export { Curve } from './extras/core/Curve.js';
export { DataUtils } from './extras/DataUtils.js';
export { ImageUtils } from './extras/ImageUtils.js';
export { ShapeUtils } from './extras/ShapeUtils.js';
export { TextureUtils } from './extras/TextureUtils.js';
export { PMREMGenerator } from './extras/PMREMGenerator.js';
export { WebGLUtils } from './renderers/webgl/WebGLUtils.js';
export { createCanvasElement } from './utils.js';
Expand Down
143 changes: 143 additions & 0 deletions src/extras/TextureUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { AlphaFormat, LuminanceFormat, LuminanceAlphaFormat, RedFormat, RedIntegerFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBAFormat, RGBAIntegerFormat, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_PVRTC_4BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, RED_GREEN_RGTC2_Format, SIGNED_RED_GREEN_RGTC2_Format, UnsignedByteType, ByteType, UnsignedShortType, ShortType, HalfFloatType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedIntType, IntType, FloatType, UnsignedInt5999Type } from '../constants.js';

/**
* Given the width, height, format, and type of a texture. Determines how many
* bytes must be used to represent the texture.
*/
function getByteLength( width, height, format, type ) {
Copy link
Collaborator

@Mugen87 Mugen87 Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this function be internal or is it required for the user to have access to it?

If possible, I would prefer to not expose it and move the logic to WebGLUtils instead. In a different PR, there is the plan to add TextureUtils as an addon in examples/jsm (see #28512 (comment)). It is not ideal to have the same module in the core and as an addon.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, I see getByteLength() is used in the new example...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is an advanced feature, I would probably not expose the helper for now and inline the size computation in the example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think addLayerUpdate has limited value if three doesn't provide clients a way to know the byte bounds of a single layer. You need TextureUtils.getByteLength to know which subarray within the CompressedArrayTexture's data to update. We could make this method internal, but then it would force clients to either implement this logic themselves or copy/paste it from the three repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And inlining would fail in the example because the KTX2Loader dynamically selects a format + type depending on the underlying WebGL context. There's no guarantee that the hard-coded byte length will be consistent on different devices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(also thanks for the quick review)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see how we can organize the code regarding #28512.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in #28512 is so compact so I think we can move it in src as well. That means there is no need to update this PR.

It's definitely good that the layer size computation in uploadTexture() is moved into a helper function.


const typeByteLength = getTextureTypeByteLength( type );

switch ( format ) {

// https://registry.khronos.org/OpenGL-Refpages/es3.0/html/glTexImage2D.xhtml
case AlphaFormat:
return width * height;
case LuminanceFormat:
return width * height;
case LuminanceAlphaFormat:
return width * height * 2;
case RedFormat:
return ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;
case RedIntegerFormat:
return ( ( width * height ) / typeByteLength.components ) * typeByteLength.byteLength;
case RGFormat:
return ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;
case RGIntegerFormat:
return ( ( width * height * 2 ) / typeByteLength.components ) * typeByteLength.byteLength;
case RGBFormat:
return ( ( width * height * 3 ) / typeByteLength.components ) * typeByteLength.byteLength;
case RGBAFormat:
return ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;
case RGBAIntegerFormat:
return ( ( width * height * 4 ) / typeByteLength.components ) * typeByteLength.byteLength;

// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_s3tc_srgb/
case RGB_S3TC_DXT1_Format:
case RGBA_S3TC_DXT1_Format:
return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;
case RGBA_S3TC_DXT3_Format:
case RGBA_S3TC_DXT5_Format:
return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;

// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_pvrtc/
case RGB_PVRTC_2BPPV1_Format:
case RGBA_PVRTC_2BPPV1_Format:
return ( Math.max( width, 16 ) * Math.max( height, 8 ) ) / 4;
case RGB_PVRTC_4BPPV1_Format:
case RGBA_PVRTC_4BPPV1_Format:
return ( Math.max( width, 8 ) * Math.max( height, 8 ) ) / 2;

// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_etc/
case RGB_ETC1_Format:
case RGB_ETC2_Format:
return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 8;
case RGBA_ETC2_EAC_Format:
return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;

// https://registry.khronos.org/webgl/extensions/WEBGL_compressed_texture_astc/
case RGBA_ASTC_4x4_Format:
return Math.floor( ( width + 3 ) / 4 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
case RGBA_ASTC_5x4_Format:
return Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 3 ) / 4 ) * 16;
case RGBA_ASTC_5x5_Format:
return Math.floor( ( width + 4 ) / 5 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
case RGBA_ASTC_6x5_Format:
return Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
case RGBA_ASTC_6x6_Format:
return Math.floor( ( width + 5 ) / 6 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
case RGBA_ASTC_8x5_Format:
return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
case RGBA_ASTC_8x6_Format:
return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
case RGBA_ASTC_8x8_Format:
return Math.floor( ( width + 7 ) / 8 ) * Math.floor( ( height + 7 ) / 8 ) * 16;
case RGBA_ASTC_10x5_Format:
return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 4 ) / 5 ) * 16;
case RGBA_ASTC_10x6_Format:
return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 5 ) / 6 ) * 16;
case RGBA_ASTC_10x8_Format:
return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 7 ) / 8 ) * 16;
case RGBA_ASTC_10x10_Format:
return Math.floor( ( width + 9 ) / 10 ) * Math.floor( ( height + 9 ) / 10 ) * 16;
case RGBA_ASTC_12x10_Format:
return Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 9 ) / 10 ) * 16;
case RGBA_ASTC_12x12_Format:
return (
Math.floor( ( width + 11 ) / 12 ) * Math.floor( ( height + 11 ) / 12 ) * 16
);

// https://registry.khronos.org/webgl/extensions/EXT_texture_compression_bptc/
case RGBA_BPTC_Format:
case RGB_BPTC_SIGNED_Format:
case RGB_BPTC_UNSIGNED_Format:
return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;

// https://registry.khronos.org/webgl/extensions/EXT_texture_compression_rgtc/
case RED_RGTC1_Format:
case SIGNED_RED_RGTC1_Format:
return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 8;
case RED_GREEN_RGTC2_Format:
case SIGNED_RED_GREEN_RGTC2_Format:
return Math.ceil( width / 4 ) * Math.ceil( height / 4 ) * 16;

}

throw new Error(
`Unable to determine texture byte length for ${format} format.`,
);

}

function getTextureTypeByteLength( type ) {

switch ( type ) {

case UnsignedByteType:
case ByteType:
return { byteLength: 1, components: 1 };
case UnsignedShortType:
case ShortType:
case HalfFloatType:
return { byteLength: 2, components: 1 };
case UnsignedShort4444Type:
case UnsignedShort5551Type:
return { byteLength: 2, components: 4 };
case UnsignedIntType:
case IntType:
case FloatType:
return { byteLength: 4, components: 1 };
case UnsignedInt5999Type:
return { byteLength: 4, components: 3 };

}

throw new Error( `Unknown texture type ${type}.` );

}

const TextureUtils = {
getByteLength,
};

export { getByteLength, TextureUtils };
59 changes: 6 additions & 53 deletions src/renderers/webgl/WebGLTextures.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { LinearFilter, LinearMipmapLinearFilter, LinearMipmapNearestFilter, Near
import { createElementNS } from '../../utils.js';
import { ColorManagement } from '../../math/ColorManagement.js';
import { Vector2 } from '../../math/Vector2.js';
import { TextureUtils } from '../../extras/TextureUtils.js';

function WebGLTextures( _gl, extensions, state, properties, capabilities, utils, info ) {

Expand Down Expand Up @@ -854,10 +855,11 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,

if ( texture.layerUpdates.size > 0 ) {

const layerSize = TextureUtils.getByteLength( mipmap.width, mipmap.height, texture.format, texture.type );

for ( const layerIndex of texture.layerUpdates ) {

const layerSize = mipmap.width * mipmap.height;
state.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, layerIndex, mipmap.width, mipmap.height, 1, glFormat, mipmap.data.slice( layerSize * layerIndex, layerSize * ( layerIndex + 1 ) ), 0, 0 );
state.compressedTexSubImage3D( _gl.TEXTURE_2D_ARRAY, i, 0, 0, layerIndex, mipmap.width, mipmap.height, 1, glFormat, mipmap.data.subarray( layerSize * layerIndex, layerSize * ( layerIndex + 1 ) ), 0, 0 );

}

Expand Down Expand Up @@ -975,60 +977,11 @@ function WebGLTextures( _gl, extensions, state, properties, capabilities, utils,

if ( texture.layerUpdates.size > 0 ) {

// When type is GL_UNSIGNED_BYTE, each of these bytes is
// interpreted as one color component, depending on format. When
// type is one of GL_UNSIGNED_SHORT_5_6_5,
// GL_UNSIGNED_SHORT_4_4_4_4, GL_UNSIGNED_SHORT_5_5_5_1, each
// unsigned value is interpreted as containing all the components
// for a single pixel, with the color components arranged
// according to format.
//
// See https://registry.khronos.org/OpenGL-Refpages/es1.1/xhtml/glTexImage2D.xml
let texelSize;
switch ( glType ) {

case _gl.UNSIGNED_BYTE:
switch ( glFormat ) {

case _gl.ALPHA:
texelSize = 1;
break;
case _gl.LUMINANCE:
texelSize = 1;
break;
case _gl.LUMINANCE_ALPHA:
texelSize = 2;
break;
case _gl.RGB:
texelSize = 3;
break;
case _gl.RGBA:
texelSize = 4;
break;

default:
throw new Error( `Unknown texel size for format ${glFormat}.` );

}

break;

case _gl.UNSIGNED_SHORT_4_4_4_4:
case _gl.UNSIGNED_SHORT_5_5_5_1:
case _gl.UNSIGNED_SHORT_5_6_5:
texelSize = 1;
break;

default:
throw new Error( `Unknown texel size for type ${glType}.` );

}

const layerSize = image.width * image.height * texelSize;
const layerSize = TextureUtils.getByteLength( mipmap.width, mipmap.height, texture.format, texture.type );

for ( const layerIndex of texture.layerUpdates ) {

state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, layerIndex, image.width, image.height, 1, glFormat, glType, image.data.slice( layerSize * layerIndex, layerSize * ( layerIndex + 1 ) ) );
state.texSubImage3D( _gl.TEXTURE_2D_ARRAY, 0, 0, 0, layerIndex, image.width, image.height, 1, glFormat, glType, image.data.subarray( layerSize * layerIndex, layerSize * ( layerIndex + 1 ) ) );

}

Expand Down