diff --git a/Sources/UntoldEngine/Renderer/RenderInitializer.swift b/Sources/UntoldEngine/Renderer/RenderInitializer.swift index 28b015e05..c6c44a8be 100644 --- a/Sources/UntoldEngine/Renderer/RenderInitializer.swift +++ b/Sources/UntoldEngine/Renderer/RenderInitializer.swift @@ -110,6 +110,30 @@ func createTexture( return texture } +/// Clears an IBL lookup texture (irradiance/specular/BRDF map) to all-zero so a failed or +/// skipped bake leaves it contributing nothing to shading, instead of the uninitialized +/// garbage Metal may leave in a freshly allocated `.shared` texture. IBL maps are always +/// `.rgba16Float` (see `WorkingColorFormats`), 8 bytes/pixel. +func zeroFillIBLTexture(_ texture: MTLTexture?) { + guard let texture else { return } + let bytesPerPixel = 8 + var mipWidth = texture.width + var mipHeight = texture.height + let zeroBuffer = [UInt8](repeating: 0, count: mipWidth * mipHeight * bytesPerPixel) + for level in 0 ..< texture.mipmapLevelCount { + zeroBuffer.withUnsafeBytes { bytes in + texture.replace( + region: MTLRegionMake2D(0, 0, mipWidth, mipHeight), + mipmapLevel: level, + withBytes: bytes.baseAddress!, + bytesPerRow: mipWidth * bytesPerPixel + ) + } + mipWidth = max(1, mipWidth / 2) + mipHeight = max(1, mipHeight / 2) + } +} + @inline(__always) func requestedOpaqueSampleCount() -> Int { #if targetEnvironment(simulator) @@ -1352,8 +1376,29 @@ func initIBLResources() { renderInfo.iblOffscreenRenderPassDescriptor.colorAttachments[2].texture = textureResources.iblBRDFMap + guard !hdrURL.isEmpty else { + // No environment is configured for the current scene (e.g. deserializeScene cleared + // hdrURL because the scene has no HDR). This is not a failure — there is simply + // nothing to bake — so skip generateHDR entirely rather than attempting it and + // logging a spurious iBLCreationFailed error on the next resize/init. + zeroFillIBLTexture(textureResources.irradianceMap) + zeroFillIBLTexture(textureResources.specularMap) + zeroFillIBLTexture(textureResources.iblBRDFMap) + iblSuccessful = false + return + } + generateHDR(hdrURL, from: hdrDirectory) + if !iblSuccessful { + // The bake failed partway through: the maps above may hold uninitialized data from + // `createTexture`, or partially-written data from an incomplete prefilter pass. Zero + // them so a failed IBL bake is ignored rather than corrupting shading with garbage. + zeroFillIBLTexture(textureResources.irradianceMap) + zeroFillIBLTexture(textureResources.specularMap) + zeroFillIBLTexture(textureResources.iblBRDFMap) + } + if iblSuccessful, let cacheKey, let irradianceMap = textureResources.irradianceMap, let specularMap = textureResources.specularMap, diff --git a/Sources/UntoldEngine/Utils/FuncUtils.swift b/Sources/UntoldEngine/Utils/FuncUtils.swift index 2b1cc09ad..6d961c742 100644 --- a/Sources/UntoldEngine/Utils/FuncUtils.swift +++ b/Sources/UntoldEngine/Utils/FuncUtils.swift @@ -344,6 +344,7 @@ public func generateHDR(_ hdrName: String, from directory: URL? = nil) { guard let envMipMapCommandBuffer: MTLCommandBuffer = renderInfo.commandQueue.makeCommandBuffer() else { handleError(.iblMipMapCreationFailed) + iblSuccessful = false return } @@ -352,6 +353,7 @@ public func generateHDR(_ hdrName: String, from directory: URL? = nil) { envMipMapCommandBuffer.makeBlitCommandEncoder() else { handleError(.iblMipMapBlitCreationFailed) + iblSuccessful = false return } @@ -371,6 +373,7 @@ public func generateHDR(_ hdrName: String, from directory: URL? = nil) { let iblPreFilterCommandBuffer: MTLCommandBuffer = renderInfo.commandQueue.makeCommandBuffer() else { handleError(.iblPreFilterCreationFailed) + iblSuccessful = false return }