Not enough imports with side-effects when import WebGPU engine separately

Hello everyone!
I am developing a game with babylon.js and recently I got a very non-intuitive and hard-to-find-out-why-that-thing-dont-work-omg bug.

To make long story short - I have a planet (non-transparent material) and clouds, that I draw using shader material on another sphere, which is a little bit bigger than planet itself. This clouds sphere material has alpha enabled, so it can be transparent. The mode is ALPHA_COMBINE, just in case.

Behind my planet, I have a sun. A sun is just nothing more but a sphere with custom shader material.So, the bug:In certain zones, where transparency of clouds sphere > ~20%, the sun silhouette becomes visible through the planet as pure-white circle.

Furthermore, my sun’s halo, which I rendered on a plane with the ALPHA_ADD mode, was always showing through the planet, everywhere, regardless of clouds. This started to happen after I decided to add custom postprocess into my game. The postprocess addition alters the rendering pipeline, as far as I understand, and for example, you have to control occlusion manually this case, using depth maps.

I spent a long time trying to figure this out. Initially, I thought I’d misconfigured the occlusion (because after adding postprocessing to a scene, you have to do it manually, transferring the depth texture to the materials of translucent objects and then cutting off the part obscured by other objects). But after two days of searching, I couldn’t fix it, even though I tried everything and burned through half my AI subscription in vain attempts to figure out what was going on. Then, completely unexpectedly, the bug completely disappeared when I added the Babylon Scene Inspector to the project.

After carefully examining what was happening to the project when adding the Scene Inspector, I concluded that there was some side effect, some package that fixed this bug, which was imported by the Inspector but, for some unknown reason, was missing from my working project without the Inspector. I should note, however, that I didn’t observe any errors anywhere. In other words, everything seemed to be working correctly for me.I created a list of all imports added when working with the Inspector and, using a binary search, found the package that, when added to my project, eliminated the bug. It turned out to be import “@babylonjs/core/Engines/AbstractEngine/abstractEngine.texture”

In my project, I don’t import the entire engine module, only the webgpu engine. Therefore, this module isn’t imported and, as a result, doesn’t call RegisterAbstractEngineTexture or implement createDepthStencilTexture. Because of this, all depth manipulation using this texture in the project itself fails. Again, I don’t get any error message; it simply doesn’t work.

Can you please fix this? Below I attach minimal example that reproduces this behavior.

import "@babylonjs/core/Culling/ray"
import "@babylonjs/core/Engines/Extensions/engine.computeShader"
import "@babylonjs/core/Engines/WebGPU/Extensions/engine.computeShader"
import "@babylonjs/core/Rendering/depthRendererSceneComponent"
import "@babylonjs/core/ShadersWGSL/ShadersInclude/meshUboDeclaration"
import "@babylonjs/core/ShadersWGSL/ShadersInclude/sceneUboDeclaration"
import {ArcRotateCamera} from "@babylonjs/core/Cameras/arcRotateCamera"
import {Camera} from "@babylonjs/core/Cameras/camera"
import {Constants} from "@babylonjs/core/Engines/constants"
import {WebGPUEngine} from "@babylonjs/core/Engines/webgpuEngine"
import {FeatureName} from "@babylonjs/core/Engines/WebGPU/webgpuConstants"
import {HemisphericLight} from "@babylonjs/core/Lights/hemisphericLight"
import {DirectionalLight} from "@babylonjs/core/Lights/directionalLight"
import {Effect} from "@babylonjs/core/Materials/effect"
import {Material} from "@babylonjs/core/Materials/material"
import {ShaderLanguage} from "@babylonjs/core/Materials/shaderLanguage"
import {ShaderMaterial} from "@babylonjs/core/Materials/shaderMaterial"
import {StandardMaterial} from "@babylonjs/core/Materials/standardMaterial"
import type {BaseTexture} from "@babylonjs/core/Materials/Textures/baseTexture"
import {RawTexture2DArray} from "@babylonjs/core/Materials/Textures/rawTexture2DArray"
import {Texture} from "@babylonjs/core/Materials/Textures/texture"
import {Color3, Color4} from "@babylonjs/core/Maths/math.color"
import {Clamp} from "@babylonjs/core/Maths/math.scalar.functions"
import {Vector3} from "@babylonjs/core/Maths/math.vector"
import {
  type CreateSphere,
  CreateSphereVertexData,
} from "@babylonjs/core/Meshes/Builders/sphereBuilder"
import {CreatePlane} from "@babylonjs/core/Meshes/Builders/planeBuilder"
import {Mesh} from "@babylonjs/core/Meshes/mesh"
import {_IsSideEffectImplemented} from "@babylonjs/core/Misc/devTools"
import {PostProcess} from "@babylonjs/core/PostProcesses/postProcess"
import type {DepthRenderer} from "@babylonjs/core/Rendering/depthRenderer"
import {Scene} from "@babylonjs/core/scene"

// For the fix validation, uncomment only this line.
// It should reproduce the same effect without importing the full Engine module.
// import "@babylonjs/core/Engines/AbstractEngine/abstractEngine.texture"

const DEBUG_SCENE_CONFIG = {
  CameraAlpha: 0.9,
  CameraBeta: 1.1,
  CameraDefaultRadius: 3.2,
  PlanetDiameter: 2,
  PlanetSegments: 64,
  PlanetCloudsSurfaceOffset: 0.01,
  PlanetAtmospherePlaneRadiusMultiplier: 1.16,
  SunDiameter: 4,
  SunHaloRadiusMultiplier: 3,
  NoiseTextureSize: 128,
  NoiseTextureLayerCount: 8,
  StatusUpdateIntervalMs: 250,
} as const

const DEBUG_ENTITY_NAMES = {
  PlanetCamera: "mainCamera",
  Planet: "Planet",
  PlanetCloudsSphere: "planet-clouds-sphere",
  PlanetAtmospherePlane: "planet-atmosphere-plane",
  AmbientLight: "ambientLight",
  SunLight: "sunLight",
  Sun: "sun",
  SunHaloPlane: "sphere-effect-plane",
} as const

const DEBUG_SHADER_DATA = {
  Attributes: {
    Position: "position",
  },
  UniformBuffers: {
    Scene: "Scene",
    Mesh: "Mesh",
  },
  Uniforms: {
    TimeSeconds: "timeSeconds",
    DiscRadiusOnPlane: "discRadiusOnPlane",
  },
  Textures: {
    CloudNoise: "cloudNoiseTexture",
    SunActivityNoise: "sunActivityNoiseTexture",
    SunHaloNoise: "sunHaloNoiseTexture",
    SceneDepth: "sceneDepthTexture",
  },
  Varyings: {
    SpherePosition: "vSpherePosition",
    PlaneUv: "planeUv",
    PostProcessUv: "vUV",
  },
} as const

const DEBUG_POSTPROCESS_SHADER_NAME = "sunVolumetricRaysPostprocess"
const DEBUG_POSTPROCESS_NAME = "sun-volumetric-rays-postprocess"

const DEBUG_POSTPROCESS_FRAGMENT_SOURCE = `
varying ${DEBUG_SHADER_DATA.Varyings.PostProcessUv}: vec2<f32>;
var textureSamplerSampler: sampler;
var textureSampler: texture_2d<f32>;

@fragment
// Pass-through color frame for debug post process.
fn main(input: FragmentInputs) -> FragmentOutputs {
  fragmentOutputs.color = textureSample(
    textureSampler,
    textureSamplerSampler,
    input.${DEBUG_SHADER_DATA.Varyings.PostProcessUv}
  );
}`

Effect.RegisterShader(
  DEBUG_POSTPROCESS_SHADER_NAME,
  DEBUG_POSTPROCESS_FRAGMENT_SOURCE,
  undefined,
  ShaderLanguage.WGSL,
)

const DEPTH_OCCLUSION_WGSL = `
var ${DEBUG_SHADER_DATA.Textures.SceneDepth}Sampler: sampler;
var ${DEBUG_SHADER_DATA.Textures.SceneDepth}: texture_2d<f32>;

// Reads the opaque scene depth at the current fragment's screen position.
fn readSceneDepthAtFragment(fragmentPosition: vec4<f32>) -> f32 {
  let depthTextureSize = vec2<f32>(textureDimensions(${DEBUG_SHADER_DATA.Textures.SceneDepth}));
  let depthUv = fragmentPosition.xy / depthTextureSize;
  if (
    depthUv.x < 0.0 ||
    depthUv.x > 1.0 ||
    depthUv.y < 0.0 ||
    depthUv.y > 1.0
  ) {
    return 1.0;
  }

  return textureSampleLevel(
    ${DEBUG_SHADER_DATA.Textures.SceneDepth},
    ${DEBUG_SHADER_DATA.Textures.SceneDepth}Sampler,
    depthUv,
    0.0
  ).r;
}

// Returns true if a transparent fragment is behind the opaque scene depth.
fn isFragmentOccludedBySceneDepth(fragmentPosition: vec4<f32>) -> bool {
  let sceneDepth = readSceneDepthAtFragment(fragmentPosition);

  return fragmentPosition.z > sceneDepth;
}`

const SPHERE_VERTEX_SOURCE = `
#include<sceneUboDeclaration>
#include<meshUboDeclaration>
attribute ${DEBUG_SHADER_DATA.Attributes.Position}: vec3<f32>;
varying ${DEBUG_SHADER_DATA.Varyings.SpherePosition}: vec3<f32>;

@vertex
// Passes normalized sphere position to the fragment shader.
fn main(input: VertexInputs) -> FragmentInputs {
  let localPosition = vertexInputs.${DEBUG_SHADER_DATA.Attributes.Position};
  vertexOutputs.${DEBUG_SHADER_DATA.Varyings.SpherePosition} = normalize(localPosition);
  vertexOutputs.position = scene.viewProjection * mesh.world * vec4<f32>(localPosition, 1.0);
}`

const PLANET_CLOUDS_FRAGMENT_SOURCE = `
varying ${DEBUG_SHADER_DATA.Varyings.SpherePosition}: vec3<f32>;
uniform ${DEBUG_SHADER_DATA.Uniforms.TimeSeconds}: f32;
var ${DEBUG_SHADER_DATA.Textures.CloudNoise}Sampler: sampler;
var ${DEBUG_SHADER_DATA.Textures.CloudNoise}: texture_2d_array<f32>;
${DEPTH_OCCLUSION_WGSL}

@fragment
// Simplified default planet cloud branch: alpha blend + manual depth discard.
fn main(input: FragmentInputs) -> FragmentOutputs {
  if (isFragmentOccludedBySceneDepth(fragmentInputs.position)) {
    discard;
  }

  let sphereCoords = normalize(fragmentInputs.${DEBUG_SHADER_DATA.Varyings.SpherePosition});
  let noiseUv = sphereCoords.xy * 0.5 + vec2<f32>(0.5);
  let noise = textureSample(
    ${DEBUG_SHADER_DATA.Textures.CloudNoise},
    ${DEBUG_SHADER_DATA.Textures.CloudNoise}Sampler,
    noiseUv,
    0
  ).r;
  let cloudDensity = smoothstep(0.42, 0.62, noise) * 0.55;
  if (cloudDensity <= 0.000001) {
    discard;
  }

  fragmentOutputs.color = vec4<f32>(vec3<f32>(1.0), cloudDensity);
}`

const PLANET_ATMOSPHERE_PLANE_VERTEX_SOURCE = `
#include<sceneUboDeclaration>
#include<meshUboDeclaration>
attribute ${DEBUG_SHADER_DATA.Attributes.Position}: vec3<f32>;
varying ${DEBUG_SHADER_DATA.Varyings.PlaneUv}: vec2<f32>;

@vertex
// Mirrors the billboard plane pattern where UV is derived from local plane position.
fn main(input: VertexInputs) -> FragmentInputs {
  let localPosition = vertexInputs.${DEBUG_SHADER_DATA.Attributes.Position};
  let worldPosition = (mesh.world * vec4<f32>(localPosition, 1.0)).xyz;

  vertexOutputs.${DEBUG_SHADER_DATA.Varyings.PlaneUv} = localPosition.xy * 0.5 + vec2<f32>(0.5);
  vertexOutputs.position = scene.viewProjection * vec4<f32>(worldPosition, 1.0);
}`

const PLANET_ATMOSPHERE_PLANE_FRAGMENT_SOURCE = `
varying ${DEBUG_SHADER_DATA.Varyings.PlaneUv}: vec2<f32>;
${DEPTH_OCCLUSION_WGSL}

@fragment
// Simplified atmosphere billboard: additive transparent layer + manual depth discard.
fn main(input: FragmentInputs) -> FragmentOutputs {
  if (isFragmentOccludedBySceneDepth(fragmentInputs.position)) {
    discard;
  }

  let centeredUv = fragmentInputs.${DEBUG_SHADER_DATA.Varyings.PlaneUv} * 2.0 - vec2<f32>(1.0);
  let radius = length(centeredUv);
  if (radius > 1.0 || radius < 0.82) {
    discard;
  }

  let alpha = (1.0 - smoothstep(0.82, 1.0, radius)) * 0.16;
  fragmentOutputs.color = vec4<f32>(vec3<f32>(0.25, 0.58, 1.0), alpha);
}`

const SUN_SURFACE_FRAGMENT_SOURCE = `
varying ${DEBUG_SHADER_DATA.Varyings.SpherePosition}: vec3<f32>;
uniform ${DEBUG_SHADER_DATA.Uniforms.TimeSeconds}: f32;
var ${DEBUG_SHADER_DATA.Textures.SunActivityNoise}Sampler: sampler;
var ${DEBUG_SHADER_DATA.Textures.SunActivityNoise}: texture_2d_array<f32>;
var ${DEBUG_SHADER_DATA.Textures.SceneDepth}Sampler: sampler;
var ${DEBUG_SHADER_DATA.Textures.SceneDepth}: texture_2d<f32>;

@fragment
// Simplified default sun surface branch.
fn main(input: FragmentInputs) -> FragmentOutputs {
  let sphereCoords = normalize(fragmentInputs.${DEBUG_SHADER_DATA.Varyings.SpherePosition});
  let noiseUv = sphereCoords.xy * 0.5 + vec2<f32>(0.5);
  let noise = textureSample(
    ${DEBUG_SHADER_DATA.Textures.SunActivityNoise},
    ${DEBUG_SHADER_DATA.Textures.SunActivityNoise}Sampler,
    noiseUv,
    0
  ).r;
  let activity = smoothstep(0.14, 0.86, noise);
  let color = mix(
    vec3<f32>(1.0, 0.22, 0.02),
    vec3<f32>(1.0, 0.92, 0.28),
    activity
  );

  fragmentOutputs.color = vec4<f32>(color, 1.0);
}`

const SUN_HALO_PLANE_VERTEX_SOURCE = `
#include<sceneUboDeclaration>
#include<meshUboDeclaration>
attribute ${DEBUG_SHADER_DATA.Attributes.Position}: vec3<f32>;
varying ${DEBUG_SHADER_DATA.Varyings.PlaneUv}: vec2<f32>;

@vertex
// Mirrors the sun halo billboard plane where UV is derived from local plane position.
fn main(input: VertexInputs) -> FragmentInputs {
  let localPosition = vertexInputs.${DEBUG_SHADER_DATA.Attributes.Position};
  let worldPosition = (mesh.world * vec4<f32>(localPosition, 1.0)).xyz;

  vertexOutputs.${DEBUG_SHADER_DATA.Varyings.PlaneUv} = localPosition.xy * 0.5 + vec2<f32>(0.5);
  vertexOutputs.position = scene.viewProjection * vec4<f32>(worldPosition, 1.0);
}`

const SUN_HALO_PLANE_FRAGMENT_SOURCE = `
varying ${DEBUG_SHADER_DATA.Varyings.PlaneUv}: vec2<f32>;
uniform ${DEBUG_SHADER_DATA.Uniforms.DiscRadiusOnPlane}: f32;
uniform ${DEBUG_SHADER_DATA.Uniforms.TimeSeconds}: f32;
var ${DEBUG_SHADER_DATA.Textures.SunHaloNoise}Sampler: sampler;
var ${DEBUG_SHADER_DATA.Textures.SunHaloNoise}: texture_2d_array<f32>;
${DEPTH_OCCLUSION_WGSL}

@fragment
// Simplified default sun halo branch.
fn main(input: FragmentInputs) -> FragmentOutputs {
  if (isFragmentOccludedBySceneDepth(fragmentInputs.position)) {
    discard;
  }

  let planeUv = fragmentInputs.${DEBUG_SHADER_DATA.Varyings.PlaneUv};
  let centeredUv = planeUv * 2.0 - vec2<f32>(1.0);
  let radius = length(centeredUv);
  if (radius > 1.0) {
    discard;
  }

  let noise = textureSample(
    ${DEBUG_SHADER_DATA.Textures.SunHaloNoise},
    ${DEBUG_SHADER_DATA.Textures.SunHaloNoise}Sampler,
    planeUv,
    0
  ).r;
  let sunDistance = radius / max(uniforms.${DEBUG_SHADER_DATA.Uniforms.DiscRadiusOnPlane}, 0.000001) - 1.0;
  var brightness = 0.0;
  if (sunDistance <= 0.0) {
    brightness = (1.0 - radius / max(uniforms.${DEBUG_SHADER_DATA.Uniforms.DiscRadiusOnPlane}, 0.000001)) * 0.28;
  } else if (sunDistance <= 1.0) {
    brightness = 0.20;
  } else {
    brightness = 0.11;
  }

  brightness = brightness * (0.76 + noise * 0.24);
  if (brightness <= 0.000001) {
    fragmentOutputs.color = vec4<f32>(0.0);
    return fragmentOutputs;
  }

  let color = mix(
    vec3<f32>(1.0, 0.24, 0.02),
    vec3<f32>(1.0, 0.78, 0.22),
    clamp(1.0 - sunDistance / 2.0, 0.0, 1.0)
  );

  fragmentOutputs.color = vec4<f32>(color * brightness, 1.0);
}`

class DebugGameObjects {
  public static engine: WebGPUEngine = null as unknown as WebGPUEngine
  public static scene: Scene = null as unknown as Scene
  public static camera: ArcRotateCamera = null as unknown as ArcRotateCamera
  public static ambientLight: HemisphericLight = null as unknown as HemisphericLight
  public static depthRenderer: DepthRenderer = null as unknown as DepthRenderer
  public static sun: DebugSun = null as unknown as DebugSun
}

/** Babylon sphere mesh that preserves the original diameter for world-size calculations. */
class DebugSphere extends Mesh {
  private readonly initialDiameter: number

  public constructor(...args: Parameters<typeof CreateSphere>) {
    const [name, options = {}, scene = null] = args

    super(name, scene)

    const sideOrientation = Mesh._GetDefaultSideOrientation(options.sideOrientation)
    const sphereOptions = {
      ...options,
      sideOrientation,
    }

    this.initialDiameter = sphereOptions.diameter || 1
    this._originalBuilderSideOrientation = sideOrientation
    CreateSphereVertexData(sphereOptions).applyToMesh(this, sphereOptions.updatable)
  }

  public get diameter(): number {
    return this.initialDiameter * Math.max(
      this.scaling.x,
      this.scaling.y,
      this.scaling.z,
    )
  }
}

class DebugSphereEffect {
  public readonly mesh: DebugSphere

  private readonly scene: Scene
  private readonly targetMesh: DebugSphere
  private currentSphereSurfaceOffset: number

  public constructor(
    targetMesh: DebugSphere,
    sphereSurfaceOffset: number,
    meshName: string,
  ) {
    this.targetMesh = targetMesh
    this.scene = targetMesh.getScene()
    this.currentSphereSurfaceOffset = sphereSurfaceOffset
    this.mesh = new DebugSphere(
      meshName,
      {
        diameter: 2,
        segments: DEBUG_SCENE_CONFIG.PlanetSegments,
        sideOrientation: Mesh.FRONTSIDE,
      },
      this.scene,
    )

    this.mesh.isPickable = false
    this.mesh.receiveShadows = false
    this.mesh.checkCollisions = false
    this.mesh.renderingGroupId = this.targetMesh.renderingGroupId
    this.mesh.parent = this.targetMesh
    this.mesh.position.set(0, 0, 0)
    this.scene.onBeforeRenderObservable.add(() => {
      this.syncWithTargetMesh()
    })
    this.syncWithTargetMesh()
  }

  private syncWithTargetMesh(): void {
    const targetRadius = this.targetMesh.diameter / 2
    const sphereRadius = targetRadius * (1 + this.currentSphereSurfaceOffset)

    this.mesh.scaling.setAll(sphereRadius)
  }
}

continuation of code

class DebugSphereEffectPlane {
  public readonly mesh: Mesh

  private readonly scene: Scene
  private readonly targetMesh: DebugSphere
  private planeRadiusMultiplier = 1
  private effectRadiusMultiplier = 1
  private readonly sphereToCamera = new Vector3()

  public constructor(targetMesh: DebugSphere, meshName: string) {
    this.targetMesh = targetMesh
    this.scene = targetMesh.getScene()
    this.mesh = CreatePlane(
      meshName,
      {
        size: 2,
        sideOrientation: Mesh.FRONTSIDE,
      },
      this.scene,
    )

    this.mesh.isPickable = false
    this.mesh.receiveShadows = false
    this.mesh.checkCollisions = false
    this.mesh.renderingGroupId = this.targetMesh.renderingGroupId
    this.mesh.billboardMode = Mesh.BILLBOARDMODE_ALL | Mesh.BILLBOARDMODE_USE_POSITION
    this.scene.onBeforeRenderObservable.add(() => {
      if (this.scene.activeCamera === null) {
        return
      }

      this.syncWithTargetMesh(this.scene.activeCamera)
    })
  }

  public setPlaneRadiusMultiplier(radiusMultiplier: number, effectRadiusMultiplier: number = 1): void {
    this.planeRadiusMultiplier = radiusMultiplier
    this.effectRadiusMultiplier = effectRadiusMultiplier
  }

  private syncWithTargetMesh(camera: Camera | null): void {
    this.targetMesh.computeWorldMatrix()

    const targetRadius = this.targetMesh.diameter / 2
    const planeRadius = targetRadius * this.planeRadiusMultiplier
    const effectRadius = targetRadius * this.effectRadiusMultiplier
    const targetPosition = this.targetMesh.getAbsolutePosition()

    this.mesh.position.copyFrom(targetPosition)
    if (camera === null) {
      this.mesh.scaling.setAll(planeRadius)
      return
    }

    camera.globalPosition.subtractToRef(targetPosition, this.sphereToCamera)
    const distanceToCamera = this.sphereToCamera.length()
    if (distanceToCamera === 0) {
      this.mesh.scaling.setAll(planeRadius)
      return
    }

    let scale = planeRadius
    if (camera.mode !== Camera.ORTHOGRAPHIC_CAMERA && distanceToCamera > effectRadius) {
      const distanceToPlane = distanceToCamera - effectRadius
      const sphereSilhouetteDistance = Math.sqrt(
        distanceToCamera * distanceToCamera - effectRadius * effectRadius,
      )

      scale *= distanceToPlane / sphereSilhouetteDistance
    }

    this.mesh.scaling.setAll(scale)
    this.sphereToCamera.normalizeFromLength(distanceToCamera)
    this.sphereToCamera.scaleAndAddToRef(effectRadius, this.mesh.position)
  }
}

class DebugPlanetCloudsSphereMaterial extends ShaderMaterial {
  public constructor(scene: Scene, cloudNoiseTexture: RawTexture2DArray) {
    super(
      "planet-clouds-sphere",
      scene,
      {
        vertexSource: SPHERE_VERTEX_SOURCE,
        fragmentSource: PLANET_CLOUDS_FRAGMENT_SOURCE,
        spectorName: "planet-clouds-sphere",
      },
      {
        attributes: [DEBUG_SHADER_DATA.Attributes.Position],
        uniforms: [
          DEBUG_SHADER_DATA.Uniforms.TimeSeconds,
        ],
        uniformBuffers: [
          DEBUG_SHADER_DATA.UniformBuffers.Scene,
          DEBUG_SHADER_DATA.UniformBuffers.Mesh,
        ],
        samplers: [
          DEBUG_SHADER_DATA.Textures.CloudNoise,
          DEBUG_SHADER_DATA.Textures.SceneDepth,
        ],
        needAlphaBlending: true,
        shaderLanguage: ShaderLanguage.WGSL,
      },
    )

    this.alphaMode = Constants.ALPHA_COMBINE
    this.backFaceCulling = false
    this.disableDepthWrite = true
    this.transparencyMode = Material.MATERIAL_ALPHABLEND
    this.setTexture(DEBUG_SHADER_DATA.Textures.CloudNoise, cloudNoiseTexture)
    this.setFloat(DEBUG_SHADER_DATA.Uniforms.TimeSeconds, 0)
  }

  public setSceneDepthTexture(texture: BaseTexture): void {
    this.setTexture(DEBUG_SHADER_DATA.Textures.SceneDepth, texture)
  }
}

class DebugPlanetAtmospherePlaneMaterial extends ShaderMaterial {
  public constructor(scene: Scene) {
    super(
      "planet-atmosphere-plane",
      scene,
      {
        vertexSource: PLANET_ATMOSPHERE_PLANE_VERTEX_SOURCE,
        fragmentSource: PLANET_ATMOSPHERE_PLANE_FRAGMENT_SOURCE,
        spectorName: "planet-atmosphere-plane",
      },
      {
        attributes: [DEBUG_SHADER_DATA.Attributes.Position],
        uniforms: [],
        uniformBuffers: [
          DEBUG_SHADER_DATA.UniformBuffers.Scene,
          DEBUG_SHADER_DATA.UniformBuffers.Mesh,
        ],
        samplers: [DEBUG_SHADER_DATA.Textures.SceneDepth],
        needAlphaBlending: true,
        shaderLanguage: ShaderLanguage.WGSL,
      },
    )

    this.alphaMode = Constants.ALPHA_ADD
    this.backFaceCulling = false
    this.disableDepthWrite = true
    this.transparencyMode = Material.MATERIAL_ALPHABLEND
  }

  public setSceneDepthTexture(texture: BaseTexture): void {
    this.setTexture(DEBUG_SHADER_DATA.Textures.SceneDepth, texture)
  }
}

class DebugSunSurfaceMaterial extends ShaderMaterial {
  public constructor(scene: Scene, activityNoiseTexture: RawTexture2DArray) {
    super(
      "sun-surface",
      scene,
      {
        vertexSource: SPHERE_VERTEX_SOURCE,
        fragmentSource: SUN_SURFACE_FRAGMENT_SOURCE,
        spectorName: "sun-surface",
      },
      {
        attributes: [DEBUG_SHADER_DATA.Attributes.Position],
        uniforms: [
          DEBUG_SHADER_DATA.Uniforms.TimeSeconds,
        ],
        uniformBuffers: [
          DEBUG_SHADER_DATA.UniformBuffers.Scene,
          DEBUG_SHADER_DATA.UniformBuffers.Mesh,
        ],
        samplers: [
          DEBUG_SHADER_DATA.Textures.SunActivityNoise,
          DEBUG_SHADER_DATA.Textures.SceneDepth,
        ],
        shaderLanguage: ShaderLanguage.WGSL,
      },
    )

    this.setTexture(DEBUG_SHADER_DATA.Textures.SunActivityNoise, activityNoiseTexture)
    this.setFloat(DEBUG_SHADER_DATA.Uniforms.TimeSeconds, 0)
  }

  public setSceneDepthTexture(texture: BaseTexture): void {
    this.setTexture(DEBUG_SHADER_DATA.Textures.SceneDepth, texture)
  }
}

class DebugSunHaloPlaneMaterial extends ShaderMaterial {
  public constructor(scene: Scene, haloNoiseTexture: RawTexture2DArray) {
    super(
      "sun-halo-plane",
      scene,
      {
        vertexSource: SUN_HALO_PLANE_VERTEX_SOURCE,
        fragmentSource: SUN_HALO_PLANE_FRAGMENT_SOURCE,
        spectorName: "sun-halo-plane",
      },
      {
        attributes: [DEBUG_SHADER_DATA.Attributes.Position],
        uniforms: [
          DEBUG_SHADER_DATA.Uniforms.DiscRadiusOnPlane,
          DEBUG_SHADER_DATA.Uniforms.TimeSeconds,
        ],
        uniformBuffers: [
          DEBUG_SHADER_DATA.UniformBuffers.Scene,
          DEBUG_SHADER_DATA.UniformBuffers.Mesh,
        ],
        samplers: [
          DEBUG_SHADER_DATA.Textures.SunHaloNoise,
          DEBUG_SHADER_DATA.Textures.SceneDepth,
        ],
        needAlphaBlending: true,
        shaderLanguage: ShaderLanguage.WGSL,
      },
    )

    this.alphaMode = Constants.ALPHA_ADD
    this.backFaceCulling = false
    this.disableDepthWrite = true
    this.transparencyMode = Material.MATERIAL_ALPHABLEND
    this.setTexture(DEBUG_SHADER_DATA.Textures.SunHaloNoise, haloNoiseTexture)
    this.setFloat(
      DEBUG_SHADER_DATA.Uniforms.DiscRadiusOnPlane,
      1 / DEBUG_SCENE_CONFIG.SunHaloRadiusMultiplier,
    )
    this.setFloat(DEBUG_SHADER_DATA.Uniforms.TimeSeconds, 0)
  }

  public setSceneDepthTexture(texture: BaseTexture): void {
    this.setTexture(DEBUG_SHADER_DATA.Textures.SceneDepth, texture)
  }
}

class DebugPlanetAtmosphere {
  private readonly scene: Scene
  private readonly cloudsSphere: DebugSphereEffect
  private readonly atmospherePlane: DebugSphereEffectPlane

  public constructor(scene: Scene, targetMesh: DebugSphere) {
    this.scene = scene
    this.cloudsSphere = new DebugSphereEffect(
      targetMesh,
      DEBUG_SCENE_CONFIG.PlanetCloudsSurfaceOffset,
      DEBUG_ENTITY_NAMES.PlanetCloudsSphere,
    )
    this.atmospherePlane = new DebugSphereEffectPlane(
      targetMesh,
      DEBUG_ENTITY_NAMES.PlanetAtmospherePlane,
    )
    this.atmospherePlane.setPlaneRadiusMultiplier(
      DEBUG_SCENE_CONFIG.PlanetAtmospherePlaneRadiusMultiplier,
      DEBUG_SCENE_CONFIG.PlanetAtmospherePlaneRadiusMultiplier,
    )
  }

  public load(): void {
    const cloudNoiseTexture = DebugTextureFactory.createNoiseTexture(
      this.scene,
      "debug-cloud-noise-texture",
      1.17,
    )
    const cloudsMaterial = new DebugPlanetCloudsSphereMaterial(this.scene, cloudNoiseTexture)
    const cloudsDepthRenderer = this.scene.enableDepthRenderer(
      undefined,
      true,
      true,
      Constants.TEXTURE_NEAREST_SAMPLINGMODE,
    )
    cloudsDepthRenderer.forceDepthWriteTransparentMeshes = false
    cloudsDepthRenderer.useOnlyInActiveCamera = true
    cloudsMaterial.setSceneDepthTexture(cloudsDepthRenderer.getDepthMap())
    this.cloudsSphere.mesh.material = cloudsMaterial

    const atmospherePlaneMaterial = new DebugPlanetAtmospherePlaneMaterial(this.scene)
    const atmosphereDepthRenderer = this.scene.enableDepthRenderer(
      undefined,
      true,
      true,
      Constants.TEXTURE_NEAREST_SAMPLINGMODE,
    )
    atmosphereDepthRenderer.forceDepthWriteTransparentMeshes = false
    atmosphereDepthRenderer.useOnlyInActiveCamera = true
    atmospherePlaneMaterial.setSceneDepthTexture(atmosphereDepthRenderer.getDepthMap())
    this.atmospherePlane.mesh.material = atmospherePlaneMaterial
  }
}

class DebugPlanet {
  public readonly mesh: DebugSphere
  public readonly scale: number
  public readonly atmosphere: DebugPlanetAtmosphere

  private constructor(mesh: DebugSphere, scale: number, atmosphere: DebugPlanetAtmosphere) {
    this.mesh = mesh
    this.scale = scale
    this.atmosphere = atmosphere
  }

  public static create(scene: Scene, scale: number = 1): DebugPlanet {
    const mesh = new DebugSphere(
      DEBUG_ENTITY_NAMES.Planet,
      {
        diameter: DEBUG_SCENE_CONFIG.PlanetDiameter,
        segments: DEBUG_SCENE_CONFIG.PlanetSegments,
      },
      scene,
    )
    mesh.scaling.setAll(scale)
    mesh.receiveShadows = false
    mesh.isPickable = false

    const material = new StandardMaterial("debug-blue-planet-material", scene)
    material.diffuseColor = new Color3(0.015, 0.18, 0.72)
    material.emissiveColor = new Color3(0.015, 0.13, 0.44)
    material.specularColor = Color3.Black()
    mesh.material = material

    return new DebugPlanet(mesh, scale, new DebugPlanetAtmosphere(scene, mesh))
  }

  public async setViewMode(): Promise<void> {
    this.atmosphere.load()
  }
}

class DebugSun {
  public readonly light: DirectionalLight
  public readonly mesh: DebugSphere
  public readonly haloPlane: DebugSphereEffectPlane

  private constructor(light: DirectionalLight, mesh: DebugSphere, haloPlane: DebugSphereEffectPlane) {
    this.light = light
    this.mesh = mesh
    this.haloPlane = haloPlane
    this.lookAt(Vector3.Zero())
  }

  public static async create(scene: Scene): Promise<DebugSun> {
    const light = new DirectionalLight(
      DEBUG_ENTITY_NAMES.SunLight,
      new Vector3(0, 0, 0),
      scene,
    )
    const sun = new DebugSphere(
      DEBUG_ENTITY_NAMES.Sun,
      {
        diameter: DEBUG_SCENE_CONFIG.SunDiameter,
        segments: DEBUG_SCENE_CONFIG.PlanetSegments,
      },
      scene,
    )
    sun.receiveShadows = false

    const activityNoiseTexture = DebugTextureFactory.createNoiseTexture(
      scene,
      "debug-sun-activity-noise-texture",
      9.41,
    )
    const surfaceMaterial = new DebugSunSurfaceMaterial(scene, activityNoiseTexture)
    const surfaceDepthRenderer = scene.enableDepthRenderer(
      undefined,
      true,
      true,
      Constants.TEXTURE_NEAREST_SAMPLINGMODE,
    )
    surfaceDepthRenderer.useOnlyInActiveCamera = true
    surfaceMaterial.setSceneDepthTexture(surfaceDepthRenderer.getDepthMap())
    sun.material = surfaceMaterial

    const haloPlane = new DebugSphereEffectPlane(sun, DEBUG_ENTITY_NAMES.SunHaloPlane)
    const haloNoiseTexture = DebugTextureFactory.createNoiseTexture(
      scene,
      "debug-sun-halo-noise-texture",
      14.73,
    )
    const haloPlaneMaterial = new DebugSunHaloPlaneMaterial(scene, haloNoiseTexture)
    const haloDepthRenderer = scene.enableDepthRenderer(
      undefined,
      true,
      true,
      Constants.TEXTURE_NEAREST_SAMPLINGMODE,
    )
    haloDepthRenderer.forceDepthWriteTransparentMeshes = false
    haloDepthRenderer.useOnlyInActiveCamera = true
    haloPlaneMaterial.setSceneDepthTexture(haloDepthRenderer.getDepthMap())
    haloPlane.mesh.material = haloPlaneMaterial
    haloPlane.setPlaneRadiusMultiplier(DEBUG_SCENE_CONFIG.SunHaloRadiusMultiplier)

    return new DebugSun(light, sun, haloPlane)
  }

  public setPosition(position: Vector3): void {
    this.mesh.position.copyFrom(position)
    this.lookAt(Vector3.Zero())
  }

  private lookAt(to: Vector3): void {
    const from = this.mesh.position
    this.light.direction = to.subtract(from).normalize()
  }
}

class DebugSunVolumetricRaysPostProcess extends PostProcess {
  public constructor(scene: Scene, camera: Camera) {
    super(
      DEBUG_POSTPROCESS_NAME,
      DEBUG_POSTPROCESS_SHADER_NAME,
      {
        camera,
        engine: scene.getEngine(),
        reusable: false,
        samplingMode: Constants.TEXTURE_BILINEAR_SAMPLINGMODE,
        samplers: ["textureSampler"],
        shaderLanguage: ShaderLanguage.WGSL,
        size: 1,
      },
    )
  }
}

class DebugTextureFactory {
  public static createNoiseTexture(scene: Scene, name: string, seed: number): RawTexture2DArray {
    const textureSize = DEBUG_SCENE_CONFIG.NoiseTextureSize
    const layerCount = DEBUG_SCENE_CONFIG.NoiseTextureLayerCount
    const data = new Uint8Array(textureSize * textureSize * layerCount * 4)

    for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) {
      for (let y = 0; y < textureSize; y++) {
        for (let x = 0; x < textureSize; x++) {
          const dataIndex = ((layerIndex * textureSize * textureSize + y * textureSize + x) * 4)
          const noiseValue = DebugTextureFactory.resolveNoiseValue(x, y, layerIndex, seed)
          const noiseByte = Math.floor(noiseValue * 255)
          data[dataIndex] = noiseByte
          data[dataIndex + 1] = noiseByte
          data[dataIndex + 2] = noiseByte
          data[dataIndex + 3] = 255
        }
      }
    }

    const texture = new RawTexture2DArray(
      data,
      textureSize,
      textureSize,
      layerCount,
      Constants.TEXTUREFORMAT_RGBA,
      scene,
      false,
      false,
      Texture.BILINEAR_SAMPLINGMODE,
      Constants.TEXTURETYPE_UNSIGNED_BYTE,
    )
    texture.name = name
    texture.wrapU = Texture.CLAMP_ADDRESSMODE
    texture.wrapV = Texture.CLAMP_ADDRESSMODE
    texture.wrapR = Texture.CLAMP_ADDRESSMODE

    return texture
  }

  private static resolveNoiseValue(x: number, y: number, layerIndex: number, seed: number): number {
    const waveNoise =
      Math.sin((x + seed * 19.17) * 0.13) * 0.20 +
      Math.sin((y - seed * 7.31) * 0.19) * 0.18 +
      Math.sin((x + y + layerIndex * 17.0 + seed) * 0.073) * 0.14
    const hashNoise = Math.sin(
      x * 12.9898 +
      y * 78.233 +
      layerIndex * 37.719 +
      seed * 91.17,
    ) * 43758.5453
    const detailedNoise = hashNoise - Math.floor(hashNoise)

    return Clamp(0.5 + waveNoise + detailedNoise * 0.36, 0, 1)
  }
}

class BabylonMainPipelineReproPage {
  private readonly canvas: HTMLCanvasElement
  private readonly statusElement: HTMLElement
  private resizeHandler: () => void = () => {}
  private planet: DebugPlanet | null = null
  private sunVolumetricRaysPostProcess: DebugSunVolumetricRaysPostProcess | null = null
  private statusUpdateTimeMs = 0

  public constructor() {
    this.canvas = document.createElement("canvas")
    this.statusElement = document.createElement("pre")
    this.canvas.addEventListener("pointerdown", (event) => event.preventDefault())
  }

  public async mount(): Promise<void> {
    this.mountPage()
    Effect.PersistentMode = true
    await this.mountScene()
  }

  private async mountScene(): Promise<void> {
    await this.setupBabylonRuntime(this.canvas)

    await new Promise<void>((resolve) => {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          resolve()
        })
      })
    })

    await this.setupPlanet()
    this.startRenderLoop()
  }

  private async setupBabylonRuntime(canvas: HTMLCanvasElement): Promise<void> {
    const supportsWebGPU = await WebGPUEngine.IsSupportedAsync
    if (!supportsWebGPU) {
      throw new Error("WebGPUEngine is not supported")
    }

    const nextEngine = new WebGPUEngine(canvas, {
      antialias: true,
      adaptToDeviceRatio: true,
      powerPreference: "high-performance",
      setMaximumLimits: true,
      deviceDescriptor: {
        requiredFeatures: [
          FeatureName.TextureFormatsTier1,
        ],
      },
    })

    await nextEngine.initAsync()
    nextEngine.useReverseDepthBuffer = false

    const scene = new Scene(nextEngine)
    scene.useRightHandedSystem = true
    scene.clearColor = new Color4(0.1, 0.1, 0.1, 1)

    DebugGameObjects.engine = nextEngine
    DebugGameObjects.scene = scene

    const resizeEngine = (): void => nextEngine.resize()
    this.resizeHandler = (): void => {
      resizeEngine()
      this.syncSceneDepthTextureSize()
    }

    window.addEventListener("resize", this.resizeHandler)
  }

  private async setupPlanet(): Promise<void> {
    const nextPlanet = DebugPlanet.create(DebugGameObjects.scene)
    this.setupCamera(this.canvas)

    const sun = await DebugSun.create(DebugGameObjects.scene)
    const planetPosition = nextPlanet.mesh.getAbsolutePosition()
    sun.setPosition(planetPosition.add(new Vector3(100, 0, 0)))
    DebugGameObjects.sun = sun
    this.sunVolumetricRaysPostProcess = new DebugSunVolumetricRaysPostProcess(
      DebugGameObjects.scene,
      DebugGameObjects.camera,
    )

    await nextPlanet.setViewMode()
    this.planet = nextPlanet
  }

  private setupCamera(canvas: HTMLCanvasElement): void {
    DebugGameObjects.camera = new ArcRotateCamera(
      DEBUG_ENTITY_NAMES.PlanetCamera,
      DEBUG_SCENE_CONFIG.CameraAlpha,
      DEBUG_SCENE_CONFIG.CameraBeta,
      DEBUG_SCENE_CONFIG.CameraDefaultRadius,
      Vector3.Zero(),
      DebugGameObjects.scene,
    )
    DebugGameObjects.camera.attachControl(canvas, true)
    DebugGameObjects.scene.activeCamera = DebugGameObjects.camera
    DebugGameObjects.ambientLight = this.useCameraLinkedAmbientLight(DebugGameObjects.camera, 1)
    DebugGameObjects.depthRenderer = DebugGameObjects.scene.enableDepthRenderer(
      undefined,
      true,
      true,
      Constants.TEXTURE_NEAREST_SAMPLINGMODE,
    )
    DebugGameObjects.depthRenderer.forceDepthWriteTransparentMeshes = false
    DebugGameObjects.depthRenderer.useOnlyInActiveCamera = true
    this.syncSceneDepthTextureSize()
  }

  private startRenderLoop(): void {
    DebugGameObjects.engine.runRenderLoop(() => {
      DebugGameObjects.scene.render()
      this.updateStatusThrottled()
    })
  }

  private mountPage(): void {
    const style = document.createElement("style")
    style.textContent = `
      html,
      body {
        width: 100%;
        height: 100%;
        margin: 0;
        overflow: hidden;
        overscroll-behavior: none;
        background: #10131a;
        color: #d8dee9;
        font-family: Consolas, monospace;
      }

      body {
        position: fixed;
        inset: 0;
      }

      .root-container {
        display: flex;
        flex-direction: row;
        width: 100%;
        height: 100vh;
        overflow: hidden;
      }

      .settings-container {
        box-sizing: border-box;
        flex: 0 0 25%;
        width: 25%;
        height: 100%;
        overflow: auto;
        border-right: 1px solid #303746;
        background: #171b24;
      }

      .canvas-container {
        position: relative;
        flex: 1 1 auto;
        min-width: 0;
        height: 100%;
        background: #151515;
      }

      pre {
        box-sizing: border-box;
        margin: 0;
        padding: 12px 16px;
        white-space: pre-wrap;
        overflow-wrap: anywhere;
        font-size: 13px;
        line-height: 1.35;
      }

      canvas {
        display: block;
        width: 100%;
        height: 100%;
        min-width: 0;
        min-height: 0;
        outline: none;
        touch-action: none;
        user-select: none;
      }
    `

    const rootContainer = document.createElement("div")
    const settingsContainer = document.createElement("div")
    const canvasContainer = document.createElement("div")
    rootContainer.className = "root-container"
    settingsContainer.className = "settings-container"
    canvasContainer.className = "canvas-container"
    settingsContainer.append(this.statusElement)
    canvasContainer.append(this.canvas)
    rootContainer.append(settingsContainer, canvasContainer)

    document.head.append(style)
    document.body.replaceChildren(rootContainer)
  }

  private useCameraLinkedAmbientLight(camera: Camera, intensity: number = 0): HemisphericLight {
    const light = new HemisphericLight(
      DEBUG_ENTITY_NAMES.AmbientLight,
      Vector3.Zero(),
      DebugGameObjects.scene,
    )
    light.diffuse = new Color3(1, 1, 1)
    light.specular = new Color3(1, 1, 1)
    light.intensity = intensity > 0 ? intensity : 0.003
    light.direction = camera.getForwardRay().direction.scale(-1)

    camera.onViewMatrixChangedObservable.add(() => {
      light.direction = camera.getForwardRay().direction.scale(-1)
    })

    return light
  }

  private syncSceneDepthTextureSize(): void {
    if (DebugGameObjects.depthRenderer === null) {
      return
    }

    const depthMap = DebugGameObjects.depthRenderer.getDepthMap()
    const currentSize = depthMap.getSize()
    const nextWidth = DebugGameObjects.engine.getRenderWidth()
    const nextHeight = DebugGameObjects.engine.getRenderHeight()

    if (currentSize.width === nextWidth && currentSize.height === nextHeight) {
      return
    }

    depthMap.resize({
      width: nextWidth,
      height: nextHeight,
    })
  }

  private updateStatusThrottled(): void {
    const nowMs = performance.now()
    if (nowMs - this.statusUpdateTimeMs < DEBUG_SCENE_CONFIG.StatusUpdateIntervalMs) {
      return
    }

    this.statusUpdateTimeMs = nowMs
    this.updateStatus()
  }

  private updateStatus(): void {
    const sideEffectRegistered = _IsSideEffectImplemented(
      DebugGameObjects.engine.createDepthStencilTexture,
    )
    const depthMap = DebugGameObjects.depthRenderer.getDepthMap()
    const hasDepthStencilTexture = depthMap.renderTarget?.depthStencilTexture != null

    this.statusElement.textContent = [
      "Babylon WebGPU main-pipeline depth repro.",
      "",
      `AbstractEngine.createDepthStencilTexture registered: ${sideEffectRegistered ? "yes" : "NO - bug path is active"}`,
      `DepthRenderer RTT depthStencilTexture exists: ${hasDepthStencilTexture ? "yes" : "NO"}`,
      `FPS: ${Math.round(DebugGameObjects.engine.getFps())}`,
      "",
      "Init order:",
      "1. create WebGPUEngine inline",
      "2. initAsync()",
      "3. create Scene(engine)",
      "4. wait two requestAnimationFrame ticks",
      "5. Planet.create()",
      "6. setupCamera(): GameCamera + ambient light + scene.enableDepthRenderer(...)",
      "7. Sun.create(): surface + halo materials call scene.enableDepthRenderer(...) themselves",
      "8. new SunVolumetricRaysPostProcess(scene, camera)",
      "9. planet.setViewMode(): clouds/atmosphere materials call scene.enableDepthRenderer(...) themselves",
      "10. engine.runRenderLoop(scene.render)",
      "",
    ].join("\n")
  }
}

void new BabylonMainPipelineReproPage().mount()

@RaananW is the best placed for this on :slight_smile:

Thanks for reporting and providing the code! i’ll look into it, i want to understand the issue thoroughly first.

So, that was quick.

This is an interesting catch. WebGL engines are importing this module, but the webgpu engine doesn’t. I don’t see why it wouldn’t TBH, so I opened a PR with a very simple fix -