[webGPU] Created a RawTexture2DArray and only accessed the mip 0 layer

The WebGPU engine uses CreateTexture2DArrayFromImageUrlsAsync and the result is “Layer 0 is normal, while layers with index > 0 are blank”.

The code is located in thinWebGPUEngine.js: The third parameter is faceIndex (which is the layer in the array texture), and it is hardcoded as 0 here.

else if (texture._source === 3 /* Raw */ || texture._source === 11 /* Raw2DArray */) {
    this._textureHelper.generateMipmaps(gpuHardwareTexture, texture.mipLevelCount, 0, commandEncoder);
}

CreateTexture2DArrayFromImageUrlsAsync has the default samplingMode set to TRILINEAR, and for a 512×512 size, it results in 10 levels of mip mapping:

Layer 0: Mip 0 is the one we uploaded (the generation is triggered once after all layers are uploaded), and mip 1 to 9 have also been generated → normal

Layers 1+ : Mip 1 to 9 were never generated. When creating the WebGPU texture, the texture is initialized to zero → sampling results in all zeros, alpha = 0 → mix(baseRgb, texColor.rgb, 0) retains the base color → the entire image disappears and this explains a phenomenon that should have been obvious: When the camera is moved closer and the parking space covers the entire screen, the second image suddenly appears (at this time, the sampling falls on mip 0). If the layer index itself is incorrect, there will definitely not be this “image appears only when viewing from a certain distance” behavior.

By the way, the path taken by WebGL2 is gl.generateMipmap(TEXTURE_2D_ARRAY), which by the specification will overwrite all layers, and there is no such proble

Temporary solution

export function generateAllLayerMipmaps(scene: Scene, atlas: RawTexture2DArray): number {
  const engine = scene.getEngine() as unknown as {
    _textureHelper?: {
      generateMipmaps(hardwareTexture: unknown, mipLevelCount: number, faceIndex: number, commandEncoder?: unknown): void
    }
  }
  const internal = (
    atlas as unknown as {
      _texture?: { mipLevelCount: number; _hardwareTexture?: unknown }
    }
  )._texture

  const helper = engine._textureHelper
  const hardwareTexture = internal?._hardwareTexture
  const mipLevelCount = internal?.mipLevelCount ?? 1
  if (!helper || !hardwareTexture || mipLevelCount <= 1) return 0

  /
  for (let layer = 0; layer < atlas.depth; layer++) {
    helper.generateMipmaps(hardwareTexture, mipLevelCount, layer)
  }
  return atlas.depth
}

use

 const textureArray = await CreateTexture2DArrayFromImageUrlsAsync(scene, ['img1.png',...])
      console.info(textureArray.depth)
      generateAllLayerMipmaps(scene, textureArray)

Confirming this, we hit the same bug independently. It isn’t limited to CreateTexture2DArrayFromImageUrlsAsync: a plain new RawTexture2DArray(..., generateMipMaps: true, ...) does the same on WebGPU. Only layer 0 gets a mip chain, and layers 1+ are black (all zeros) from mip 1 down. WebGL2 is fine.

In our game, a terrain shader samples a 2D-array of ground textures, and every layer except the first turned black a few metres from the camera.

Minimal repro, no image files needed (paste into the Playground with the WebGPU engine). Each layer is a solid colour, and we sample layer 1 at mip 3:

const createScene = () => {
  const scene = new BABYLON.Scene(engine);
  const camera = new BABYLON.ArcRotateCamera("c", -Math.PI / 2, Math.PI / 2, 40, BABYLON.Vector3.Zero(), scene);
  camera.attachControl(canvas, true);
  // 3 layers, 256x256: red, green, blue.
  const size = 256, layers = 3, data = new Uint8Array(size * size * 4 * layers);
  for (let l = 0; l < layers; l++) for (let i = 0; i < size * size; i++)
    data.set([l === 0 ? 255 : 0, l === 1 ? 255 : 0, l === 2 ? 255 : 0, 255], (l * size * size + i) * 4);
  const arr = new BABYLON.RawTexture2DArray(data, size, size, layers, BABYLON.Constants.TEXTUREFORMAT_RGBA, scene,
    true /* generateMipMaps */, false, BABYLON.Texture.TRILINEAR_SAMPLINGMODE);
  BABYLON.Effect.ShadersStore.arrVertexShader = `precision highp float; attribute vec3 position; attribute vec2 uv;
    uniform mat4 worldViewProjection; varying vec2 vUV;
    void main() { vUV = uv; gl_Position = worldViewProjection * vec4(position, 1.0); }`;
  BABYLON.Effect.ShadersStore.arrFragmentShader = `precision highp float; precision highp sampler2DArray;
    uniform sampler2DArray arr; varying vec2 vUV;
    void main() { gl_FragColor = textureLod(arr, vec3(vUV, 1.0), 3.0); }`; // layer 1, mip 3
  const mat = new BABYLON.ShaderMaterial("m", scene, "arr", { attributes: ["position", "uv"], uniforms: ["worldViewProjection"], samplers: ["arr"] });
  mat.setTexture("arr", arr);
  BABYLON.MeshBuilder.CreatePlane("p", { size: 10 }, scene).material = mat;
  return scene;
};
  • Expected (and on WebGL2): a green plane. Centre pixel [0, 255, 0].
  • WebGPU: a black plane. Centre pixel [0, 0, 0]. Sampling layer 0 at mip 3 gives red as expected.

We measured this on 9.12.0 (Chrome, Dawn/Vulkan, Intel Iris Xe). The same lines are unchanged in the 9.31.0 npm package and on master. #18941 recently added a 3D-texture branch to _generateMipmaps, but the Raw2DArray branch still passes layer 0.

A possible fix in ThinWebGPUEngine._generateMipmaps, looping layers the way generateCubeMipmaps loops faces:

} else if (texture._source === InternalTextureSource.Raw2DArray) {
    for (let layer = 0; layer < texture.depth; ++layer) {
        this._textureHelper.generateMipmaps(gpuHardwareTexture, texture.mipLevelCount, layer, commandEncoder);
    }
} else if (texture._source === InternalTextureSource.Raw) {
    this._textureHelper.generateMipmaps(gpuHardwareTexture, texture.mipLevelCount, 0, commandEncoder);
}

Until then, we use a workaround like @John-D-a-i’s: wrap engine._generateMipmaps so a Raw2DArray texture also builds layers 1..depth-1. That works fine.

I’ll take a look at this, thanks for reporting!

Thanks for the detailed report and repro! You’re right: the WebGPU path was only generating mipmaps for layer 0, leaving the higher mip levels of the other layers empty.

I’ve opened a fix here: PR #19009. It generates mipmaps for every layer while preserving the configured mip count, covering both direct RawTexture2DArray usage and CreateTexture2DArrayFromImageUrlsAsync.