diff --git a/Sources/UntoldEngine/Shaders/LightShader.metal b/Sources/UntoldEngine/Shaders/LightShader.metal index a6be4aee0..15c3a5b40 100644 --- a/Sources/UntoldEngine/Shaders/LightShader.metal +++ b/Sources/UntoldEngine/Shaders/LightShader.metal @@ -751,8 +751,6 @@ fragment TBDRLightOutput fragmentLightShaderTBDR( color.diff += areaLightColor.diff; color.spec += areaLightColor.spec; - // set emissive to zero for now - need to revisit this - emissive = 0.0; float3 finalRGB = (float3)color.diff + color.spec + indirectLighting + emissive; TBDRLightOutput out; diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air index cadabab69..6404a45bb 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib index f46314ed4..31967b4bd 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-iossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-iossim.metallib index 4181b771a..647c011c1 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-iossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-iossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air index ca8eb58a5..ff5a099b0 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib index 9b6644279..4deb1952c 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air index 2161de5de..0a640dd38 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib index ff4d91f2a..93a90e9ba 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air index c10afd569..17605fa1a 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib index e709987f5..490114adf 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air index 16d45fb80..4c92d0d75 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib index c9b28398b..f9e5194c9 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib index ba04641af..090971b59 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib differ diff --git a/Tests/UntoldEngineRenderTests/TBDREmissiveCompositeTest.swift b/Tests/UntoldEngineRenderTests/TBDREmissiveCompositeTest.swift new file mode 100644 index 000000000..9e746d6e4 --- /dev/null +++ b/Tests/UntoldEngineRenderTests/TBDREmissiveCompositeTest.swift @@ -0,0 +1,118 @@ +// +// TBDREmissiveCompositeTest.swift +// UntoldEngine +// +// Copyright (C) Untold Engine Studios +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Metal +import simd +@testable import UntoldEngine +import XCTest + +/// Regression test for the TBDR light pass zeroing G-buffer emissive before +/// compositing: material emissive must reach the deferred lit output. +final class TBDREmissiveCompositeTest: BaseRenderSetup { + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() async throws { + try await super.tearDown() + destroyAllEntities() + } + + override func initializeAssets() {} + + private func renderFrame() { + setVisibleEntities() + renderer.draw(in: renderer.metalView) + + let frameDone = expectation(description: "Frame rendered") + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { frameDone.fulfill() } + wait(for: [frameDone], timeout: TimeInterval(timeoutFactor)) + } + + private func halfToFloat(_ half: UInt16) -> Float { + let sign = UInt32(half >> 15) & 0x1 + let exponent = UInt32(half >> 10) & 0x1F + var mantissa = UInt32(half) & 0x3FF + + let bits: UInt32 + if exponent == 0 { + if mantissa == 0 { + bits = sign << 31 + } else { + var e: UInt32 = 127 - 15 + 1 + while mantissa & 0x400 == 0 { + mantissa <<= 1 + e -= 1 + } + bits = (sign << 31) | (e << 23) | ((mantissa & 0x3FF) << 13) + } + } else if exponent == 0x1F { + bits = (sign << 31) | (0xFF << 23) | (mantissa << 13) + } else { + bits = (sign << 31) | ((exponent + 127 - 15) << 23) | (mantissa << 13) + } + return Float(bitPattern: bits) + } + + /// Average color of a small region at the center of an rgba16Float texture. + private func centerAverageColor(of texture: MTLTexture, regionSize: Int = 16) throws -> simd_float3 { + XCTAssertEqual(texture.pixelFormat, .rgba16Float) + XCTAssertNotEqual(texture.storageMode, .private, "Texture must be CPU-readable") + + let originX = texture.width / 2 - regionSize / 2 + let originY = texture.height / 2 - regionSize / 2 + var data = [UInt16](repeating: 0, count: regionSize * regionSize * 4) + data.withUnsafeMutableBytes { buffer in + texture.getBytes( + buffer.baseAddress!, + bytesPerRow: regionSize * 8, + from: MTLRegionMake2D(originX, originY, regionSize, regionSize), + mipmapLevel: 0 + ) + } + + var sum = simd_float3.zero + for pixel in 0 ..< (regionSize * regionSize) { + let base = pixel * 4 + sum += simd_float3( + halfToFloat(data[base]), + halfToFloat(data[base + 1]), + halfToFloat(data[base + 2]) + ) + } + return sum / Float(regionSize * regionSize) + } + + /// Renders a screen-filling cube twice — without and with red material + /// emissive — and asserts the lit output's red channel brightens, so a + /// light shader that zeroes G-buffer emissive fails this test. + func testEmissiveMaterialReachesLitOutput() throws { + let camera = findGameCamera() + cameraLookAt(entityId: camera, eye: simd_float3(0, 0, 3), target: .zero, up: simd_float3(0, 1, 0)) + + let cube = createEntity() + setEntityMeshDirect(entityId: cube, meshes: BasicPrimitives.createCube(extent: 1.0), assetName: "EmissiveCube") + + renderFrame() + let litOutput = try XCTUnwrap(textureResources.deferredColorMap, "Deferred color map should exist") + let baseline = try centerAverageColor(of: litOutput) + + updateMaterialEmmisive(entityId: cube, emmissive: simd_float3(3, 0, 0)) + renderFrame() + let withEmissive = try centerAverageColor(of: litOutput) + + XCTAssertGreaterThan(withEmissive.x, baseline.x + 0.1, + "Red emissive must be composited into the TBDR light pass output") + XCTAssertEqual(withEmissive.y, baseline.y, accuracy: 0.05, + "Red-only emissive should not brighten the green channel") + XCTAssertEqual(withEmissive.z, baseline.z, accuracy: 0.05, + "Red-only emissive should not brighten the blue channel") + } +}