Exemple #1
0
void FTextureSource::Compress()
{
	if (CanPNGCompress())
	{
		uint8* BulkDataPtr = (uint8*)BulkData.Lock(LOCK_READ_WRITE);
		IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>( FName("ImageWrapper") );
		IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper( EImageFormat::PNG );
		// TODO: TSF_BGRA8 is stored as RGBA, so the R and B channels are swapped in the internal png. Should we fix this?
		ERGBFormat::Type RawFormat = (Format == TSF_G8) ? ERGBFormat::Gray : ERGBFormat::RGBA;
		if ( ImageWrapper.IsValid() && ImageWrapper->SetRaw( BulkDataPtr, BulkData.GetBulkDataSize(), SizeX, SizeY, RawFormat, Format == TSF_RGBA16 ? 16 : 8 ) )
		{
			const TArray<uint8>& CompressedData = ImageWrapper->GetCompressed();
			if ( CompressedData.Num() > 0 )
			{
				BulkDataPtr = (uint8*)BulkData.Realloc(CompressedData.Num());
				FMemory::Memcpy(BulkDataPtr, CompressedData.GetTypedData(), CompressedData.Num());
				BulkData.Unlock();
				bPNGCompressed = true;

				BulkData.StoreCompressedOnDisk(ECompressionFlags::COMPRESS_None);
			}
		}
	}
	else
	{
		//Can't PNG compress so just zlib compress the lot when its serialized out to disk. 
		BulkData.StoreCompressedOnDisk(ECompressionFlags::COMPRESS_ZLIB);
	}
}
void FSSTBatchCombinerModule::PluginButtonClicked()
{
	TArray<FString> OutFileNames;
	TArray<FString> OutFileNames2;
	FDesktopPlatformModule::Get()->OpenFileDialog(nullptr, "select First Image Files", "", "", "Image Files (*.png)|*.png", 1, OutFileNames);
	FDesktopPlatformModule::Get()->OpenFileDialog(nullptr, "select Second Image Files", "", "", "Image Files (*.png)|*.png", 1, OutFileNames2);
	
	if (OutFileNames.Num() && OutFileNames2.Num())
	{
		if (OutFileNames.Num() != OutFileNames2.Num())
		{
			FString DialogText = "Error! first set quantity does not match second set!";
			FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(DialogText));
			return;
		}

		IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));

		for (int32 i = 0; i < OutFileNames.Num(); i++)
		{
			TArray<uint8> RawFileData;
			TArray<uint8> RawFileData2;

			if (FFileHelper::LoadFileToArray(RawFileData, *OutFileNames[i]) && FFileHelper::LoadFileToArray(RawFileData2, *OutFileNames2[i]))
			{
				IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
				IImageWrapperPtr ImageWrapper2 = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);

				if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num())
					&& ImageWrapper2.IsValid() && ImageWrapper2->SetCompressed(RawFileData2.GetData(), RawFileData2.Num()))
				{
					const TArray<uint8>* RawData = nullptr;
					const TArray<uint8>* RawData2 = nullptr;
					if (ImageWrapper->GetRaw(ERGBFormat::BGRA, ImageWrapper->GetBitDepth(), RawData)
						&& ImageWrapper2->GetRaw(ERGBFormat::BGRA, ImageWrapper2->GetBitDepth(), RawData2))
					{
						
						uint32 ImageWidth = ImageWrapper->GetWidth();
						uint32 ImageHeight = ImageWrapper->GetHeight();
						uint32 ImageWidth2 = ImageWrapper2->GetWidth();
						uint32 ImageHeight2 = ImageWrapper2->GetHeight();

						if ((ImageWidth != ImageWidth2) || (ImageHeight != ImageHeight2))
						{
							FString DialogText = "Error! Image dimensions do not match for frame " + FString::FromInt(i);
							FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(DialogText));
							return;
						}

						TArray<uint8> newdata;
						newdata = *RawData;
						
						for (int32 a = 0; a < RawData->Num() - 4; a = a + 4)
						{
							newdata[a + 1] = (*RawData2)[a];
						}
						//save
						ImageWrapper->SetRaw(newdata.GetData(), newdata.GetAllocatedSize(), ImageWidth, ImageHeight, ERGBFormat::BGRA, 8);
						const TArray<uint8>& PNGData = ImageWrapper->GetCompressed(100);
						FFileHelper::SaveArrayToFile(PNGData, *OutFileNames[i]);
					} //if imagewrapper.getraw
				} //if imagewrapper.valid
			} //if load file
		}// for files loop
	} //if files
}
static bool CompressSliceToASTC(
	const void* SourceData,
	int32 SizeX,
	int32 SizeY,
	FString CompressionParameters,
	TArray<uint8>& OutCompressedData
)
{
	// Always Y-invert the image prior to compression for proper orientation post-compression
	uint8 LineBuffer[16384 * 4];
	uint32 LineSize = SizeX * 4;
	for (int32 LineIndex = 0; LineIndex < (SizeY / 2); LineIndex++)
	{
		uint8* LineData0 = ((uint8*)SourceData) + (LineSize * LineIndex);
		uint8* LineData1 = ((uint8*)SourceData) + (LineSize * (SizeY - LineIndex - 1));
		FMemory::Memcpy(LineBuffer, LineData0,  LineSize);
		FMemory::Memcpy(LineData0,  LineData1,  LineSize);
		FMemory::Memcpy(LineData1,  LineBuffer, LineSize);
	}
	
	// Compress and retrieve the PNG data to write out to disk
	IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
	ImageWrapper->SetRaw(SourceData, SizeX * SizeY * 4, SizeX, SizeY, ERGBFormat::RGBA, 8);
	const TArray<uint8>& FileData = ImageWrapper->GetCompressed();
	int32 FileDataSize = FileData.Num();

	FGuid Guid;
	FPlatformMisc::CreateGuid(Guid);
	FString InputFilePath = FString::Printf(TEXT("Cache/%08x-%08x-%08x-%08x-RGBToASTCIn.png"), Guid.A, Guid.B, Guid.C, Guid.D);
	FString OutputFilePath = FString::Printf(TEXT("Cache/%08x-%08x-%08x-%08x-RGBToASTCOut.astc"), Guid.A, Guid.B, Guid.C, Guid.D);

	InputFilePath  = FPaths::GameIntermediateDir() + InputFilePath;
	OutputFilePath = FPaths::GameIntermediateDir() + OutputFilePath;

	FArchive* PNGFile = NULL;
	while (!PNGFile)
	{
		PNGFile = IFileManager::Get().CreateFileWriter(*InputFilePath);   // Occasionally returns NULL due to error code ERROR_SHARING_VIOLATION
		FPlatformProcess::Sleep(0.01f);                             // ... no choice but to wait for the file to become free to access
	}
	PNGFile->Serialize((void*)&FileData[0], FileDataSize);
	delete PNGFile;

	// Compress PNG file to ASTC (using the reference astcenc.exe from ARM)
	FString Params = FString::Printf(TEXT("-c %s %s %s"),
		*InputFilePath,
		*OutputFilePath,
		*CompressionParameters
	);

	UE_LOG(LogTextureFormatASTC, Display, TEXT("Compressing to ASTC (%s)..."), *CompressionParameters);

	// Start Compressor
#if PLATFORM_MAC
	FString CompressorPath(FPaths::EngineDir() + TEXT("Binaries/ThirdParty/ARM/Mac/astcenc"));
#elif PLATFORM_LINUX
	FString CompressorPath(FPaths::EngineDir() + TEXT("Binaries/ThirdParty/ARM/Linux32/astcenc"));
#elif PLATFORM_WINDOWS
	FString CompressorPath(FPaths::EngineDir() + TEXT("Binaries/ThirdParty/ARM/Win32/astcenc.exe"));
#else
#error Unsupported platform
#endif
	FProcHandle Proc = FPlatformProcess::CreateProc(*CompressorPath, *Params, true, false, false, NULL, -1, NULL, NULL);

	// Failed to start the compressor process
	if (!Proc.IsValid())
	{
		UE_LOG(LogTextureFormatASTC, Error, TEXT("Failed to start astcenc for compressing images (%s)"), *CompressorPath);
		return false;
	}

	// Wait for the process to complete
	int ReturnCode;
	while (!FPlatformProcess::GetProcReturnCode(Proc, &ReturnCode))
	{
		FPlatformProcess::Sleep(0.01f);
	}

	// Did it work?
	bool bConversionWasSuccessful = (ReturnCode == 0);

	// Open compressed file and put the data in OutCompressedImage
	if (bConversionWasSuccessful)
	{
		// Get raw file data
		TArray<uint8> ASTCData;
		FFileHelper::LoadFileToArray(ASTCData, *OutputFilePath);
			
		// Process it
		FASTCHeader* Header = (FASTCHeader*)ASTCData.GetData();
			
		// Fiddle with the texel count data to get the right value
		uint32 TexelCountX =
			(Header->TexelCountX[0] <<  0) + 
			(Header->TexelCountX[1] <<  8) + 
			(Header->TexelCountX[2] << 16);
		uint32 TexelCountY =
			(Header->TexelCountY[0] <<  0) + 
			(Header->TexelCountY[1] <<  8) + 
			(Header->TexelCountY[2] << 16);
		uint32 TexelCountZ =
			(Header->TexelCountZ[0] <<  0) + 
			(Header->TexelCountZ[1] <<  8) + 
			(Header->TexelCountZ[2] << 16);

//		UE_LOG(LogTextureFormatASTC, Display, TEXT("    Compressed Texture Header:"));
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("             Magic: %x"), Header->Magic);
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("        BlockSizeX: %u"), Header->BlockSizeX);
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("        BlockSizeY: %u"), Header->BlockSizeY);
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("        BlockSizeZ: %u"), Header->BlockSizeZ);
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("       TexelCountX: %u"), TexelCountX);
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("       TexelCountY: %u"), TexelCountY);
//		UE_LOG(LogTextureFormatASTC, Display, TEXT("       TexelCountZ: %u"), TexelCountZ);

		// Calculate size of this mip in blocks
		uint32 MipSizeX = (TexelCountX + Header->BlockSizeX - 1) / Header->BlockSizeX;
		uint32 MipSizeY = (TexelCountY + Header->BlockSizeY - 1) / Header->BlockSizeY;

		// A block is always 16 bytes
		uint32 MipSize = MipSizeX * MipSizeY * 16;

		// Copy the compressed data
		OutCompressedData.Empty(MipSize);
		OutCompressedData.AddUninitialized(MipSize);
		void* MipData = OutCompressedData.GetData();

		// Calculate the offset to get to the mip data
		check(sizeof(FASTCHeader) == 16);
		check(ASTCData.Num() == (sizeof(FASTCHeader) + MipSize));
		FMemory::Memcpy(MipData, ASTCData.GetData() + sizeof(FASTCHeader), MipSize);
	}
	else
	{
		UE_LOG(LogTextureFormatASTC, Error, TEXT("ASTC encoder failed with return code %d, mip size (%d, %d)"), ReturnCode, SizeX, SizeY);
		IFileManager::Get().Delete(*InputFilePath);
		IFileManager::Get().Delete(*OutputFilePath);
		return false;
	}
		
	// Delete intermediate files
	IFileManager::Get().Delete(*InputFilePath);
	IFileManager::Get().Delete(*OutputFilePath);
	return true;
}
void USceneCapturer::CaptureComponent( int32 CurrentHorizontalStep, int32 CurrentVerticalStep, FString Folder, USceneCaptureComponent2D* CaptureComponent, TArray<FColor>& Atlas )
{
	TArray<FColor> SurfaceData;

	{
		SCOPE_CYCLE_COUNTER( STAT_SPReadStrip );
		FTextureRenderTargetResource* RenderTarget = CaptureComponent->TextureTarget->GameThread_GetRenderTargetResource();

		//TODO: ikrimae: Might need to validate that this divides evenly. Might not matter
		int32 CenterX = CaptureWidth / 2;
		int32 CenterY = CaptureHeight / 2;

		SurfaceData.AddUninitialized( StripWidth * StripHeight );

		// Read pixels
		FIntRect Area( CenterX - ( StripWidth / 2 ), CenterY - ( StripHeight / 2 ), CenterX + ( StripWidth / 2 ), CenterY + ( StripHeight / 2) );
        auto readSurfaceDataFlags = FReadSurfaceDataFlags();
        readSurfaceDataFlags.SetLinearToGamma(false);
		RenderTarget->ReadPixelsPtr( SurfaceData.GetData(), readSurfaceDataFlags, Area );
	}

	// Copy off strip to atlas texture
	CopyToUnprojAtlas( CurrentHorizontalStep, CurrentVerticalStep, Atlas, SurfaceData );

	if( FStereoPanoramaManager::GenerateDebugImages->GetInt() != 0 )
	{
		SCOPE_CYCLE_COUNTER( STAT_SPSavePNG );

		// Generate name
		FString TickString = FString::Printf( TEXT( "_%05d_%04d_%04d" ), CurrentFrameCount, CurrentHorizontalStep, CurrentVerticalStep );
		FString CaptureName = OutputDir / Timestamp / Folder / TickString + TEXT( ".png" );
		UE_LOG( LogStereoPanorama, Log, TEXT( "Writing snapshot: %s" ), *CaptureName );

		// Write out PNG
        if (FStereoPanoramaManager::GenerateDebugImages->GetInt() == 2)
        {
            //Read Whole Capture Buffer
		    IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper( EImageFormat::PNG );

            TArray<FColor> SurfaceDataWhole;
            SurfaceDataWhole.AddUninitialized(CaptureWidth * CaptureHeight);
            // Read pixels
            FTextureRenderTargetResource* RenderTarget = CaptureComponent->TextureTarget->GameThread_GetRenderTargetResource();
            RenderTarget->ReadPixelsPtr(SurfaceDataWhole.GetData(), FReadSurfaceDataFlags());

            // Force alpha value
            if (bForceAlpha)
            {
                for (FColor& Color : SurfaceDataWhole)
                {
                    Color.A = 255;
                }
            }

            ImageWrapper->SetRaw(SurfaceDataWhole.GetData(), SurfaceDataWhole.GetAllocatedSize(), CaptureWidth, CaptureHeight, ERGBFormat::BGRA, 8);
            const TArray<uint8>& PNGData = ImageWrapper->GetCompressed(100);

            FFileHelper::SaveArrayToFile(PNGData, *CaptureName);
            ImageWrapper.Reset();
        }
        else
        {
            if (bForceAlpha)
            {
                for (FColor& Color : SurfaceData)
                {
                    Color.A = 255;
                }
            }

            IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper(EImageFormat::PNG);
            ImageWrapper->SetRaw(SurfaceData.GetData(), SurfaceData.GetAllocatedSize(), StripWidth, StripHeight, ERGBFormat::BGRA, 8);
		    const TArray<uint8>& PNGData = ImageWrapper->GetCompressed(100);

		    FFileHelper::SaveArrayToFile( PNGData, *CaptureName );
		    ImageWrapper.Reset();
	    }
    }
}
TArray<FColor> USceneCapturer::SaveAtlas(FString Folder, const TArray<FColor>& SurfaceData)
{
	SCOPE_CYCLE_COUNTER( STAT_SPSavePNG );
	
    TArray<FColor> SphericalAtlas;
    SphericalAtlas.AddZeroed(SphericalAtlasWidth * SphericalAtlasHeight);

    const FVector2D slicePlaneDim = FVector2D(
        2.0f * FMath::Tan(FMath::DegreesToRadians(sliceHFov) / 2.0f),
        2.0f * FMath::Tan(FMath::DegreesToRadians(sliceVFov) / 2.0f));

    //For each direction,
    //    Find corresponding slice
    //    Calculate intersection of slice plane
    //    Calculate intersection UVs by projecting onto plane tangents
    //    Supersample that UV coordinate from the unprojected atlas
    {
        SCOPE_CYCLE_COUNTER(STAT_SPSampleSpherical);
        // Dump out how long the process took
        const FDateTime SamplingStartTime = FDateTime::UtcNow();
        UE_LOG(LogStereoPanorama, Log, TEXT("Sampling atlas..."));

        for (int32 y = 0; y < SphericalAtlasHeight; y++)
        {
            for (int32 x = 0; x < SphericalAtlasWidth; x++)
            {
                FLinearColor samplePixelAccum = FLinearColor(0, 0, 0, 0);

                //TODO: ikrimae: Seems that bilinear filtering sans supersampling is good enough. Supersampling sans bilerp seems best.
                //               After more tests, come back to optimize by folding supersampling in and remove this outer sampling loop.
                const auto& ssPattern = g_ssPatterns[SSMethod];

                for (int32 SampleCount = 0; SampleCount < ssPattern.numSamples; SampleCount++)
                {
                    const float sampleU = ((float)x + ssPattern.ssOffsets[SampleCount].X) / SphericalAtlasWidth;
                    const float sampleV = ((float)y + ssPattern.ssOffsets[SampleCount].Y) / SphericalAtlasHeight;

                    const float sampleTheta = sampleU * 360.0f;
                    const float samplePhi = sampleV * 180.0f;

                    const FVector sampleDir = FVector(
                        FMath::Sin(FMath::DegreesToRadians(samplePhi)) * FMath::Cos(FMath::DegreesToRadians(sampleTheta)),
                        FMath::Sin(FMath::DegreesToRadians(samplePhi)) * FMath::Sin(FMath::DegreesToRadians(sampleTheta)),
                        FMath::Cos(FMath::DegreesToRadians(samplePhi)));


                    //TODO: ikrimae: ugh, ugly.
                    const int32 sliceXIndex = FMath::TruncToInt(FRotator::ClampAxis(sampleTheta + hAngIncrement / 2.0f) / hAngIncrement);
                    int32 sliceYIndex = 0;

                    //Slice Selection = slice with max{sampleDir dot  sliceNormal }
                    {
                        float largestCosAngle = 0;
                        for (int VerticalStep = 0; VerticalStep < NumberOfVerticalSteps; VerticalStep++)
                        {
                            const FVector2D sliceCenterThetaPhi = FVector2D(
                                hAngIncrement * sliceXIndex,
                                vAngIncrement * VerticalStep);

                            //TODO: ikrimae: There has got to be a faster way. Rethink reparametrization later
                            const FVector sliceDir = FVector(
                                FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                                FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                                FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)));

                            const float cosAngle = sampleDir | sliceDir;

                            if (cosAngle > largestCosAngle)
                            {
                                largestCosAngle = cosAngle;
                                sliceYIndex = VerticalStep;
                            }
                        }
                    }


                    const FVector2D sliceCenterThetaPhi = FVector2D(
                        hAngIncrement * sliceXIndex,
                        vAngIncrement * sliceYIndex);

                    //TODO: ikrimae: Reparameterize with an inverse mapping (e.g. project from slice pixels onto final u,v coordinates.
                    //               Should make code simpler and faster b/c reduces to handful of sin/cos calcs per slice. 
                    //               Supersampling will be more difficult though.

                    const FVector sliceDir = FVector(
                        FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                        FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                        FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)));

                    const FPlane slicePlane = FPlane(sliceDir, -sliceDir);

                    //Tangents from partial derivatives of sphere equation
                    const FVector slicePlanePhiTangent = FVector(
                        FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                        FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                        -FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y))).GetSafeNormal();

                    //Should be reconstructed to get around discontinuity of theta tangent at nodal points
                    const FVector slicePlaneThetaTangent = (sliceDir ^ slicePlanePhiTangent).GetSafeNormal();
                    //const FVector slicePlaneThetaTangent = FVector(
                    //    -FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                    //    FMath::Sin(FMath::DegreesToRadians(sliceCenterThetaPhi.Y)) * FMath::Cos(FMath::DegreesToRadians(sliceCenterThetaPhi.X)),
                    //    0).SafeNormal();

                    check(!slicePlaneThetaTangent.IsZero() && !slicePlanePhiTangent.IsZero());

                    const double t = (double)-slicePlane.W / (sampleDir | sliceDir);
                    const FVector sliceIntersection = FVector(t * sampleDir.X, t * sampleDir.Y, t * sampleDir.Z);

                    //Calculate scalar projection of sliceIntersection onto tangent vectors. a dot b / |b| = a dot b when tangent vectors are normalized
                    //Then reparameterize to U,V of the sliceplane based on slice plane dimensions
                    const float sliceU = (sliceIntersection | slicePlaneThetaTangent) / slicePlaneDim.X;
                    const float sliceV = (sliceIntersection | slicePlanePhiTangent) / slicePlaneDim.Y;

                    check(sliceU >= -(0.5f + KINDA_SMALL_NUMBER) &&
                        sliceU <= (0.5f + KINDA_SMALL_NUMBER));

                    check(sliceV >= -(0.5f + KINDA_SMALL_NUMBER) &&
                        sliceV <= (0.5f + KINDA_SMALL_NUMBER));

                    //TODO: ikrimae: Supersample/bilinear filter
                    const int32 slicePixelX = FMath::TruncToInt(dbgMatchCaptureSliceFovToAtlasSliceFov ? sliceU * StripWidth : sliceU * CaptureWidth);
                    const int32 slicePixelY = FMath::TruncToInt(dbgMatchCaptureSliceFovToAtlasSliceFov ? sliceV * StripHeight : sliceV * CaptureHeight);

                    FLinearColor slicePixelSample;

                    if (bEnableBilerp)
                    {
                        //TODO: ikrimae: Clean up later; too tired now
                        const int32 sliceCenterPixelX = (sliceXIndex + 0.5f) * StripWidth;
                        const int32 sliceCenterPixelY = (sliceYIndex + 0.5f) * StripHeight;

                        const FIntPoint atlasSampleTL(sliceCenterPixelX + FMath::Clamp(slicePixelX    , -StripWidth/2, StripWidth/2), sliceCenterPixelY + FMath::Clamp(slicePixelY    , -StripHeight/2, StripHeight/2));
                        const FIntPoint atlasSampleTR(sliceCenterPixelX + FMath::Clamp(slicePixelX + 1, -StripWidth/2, StripWidth/2), sliceCenterPixelY + FMath::Clamp(slicePixelY    , -StripHeight/2, StripHeight/2));
                        const FIntPoint atlasSampleBL(sliceCenterPixelX + FMath::Clamp(slicePixelX    , -StripWidth/2, StripWidth/2), sliceCenterPixelY + FMath::Clamp(slicePixelY + 1, -StripHeight/2, StripHeight/2));
                        const FIntPoint atlasSampleBR(sliceCenterPixelX + FMath::Clamp(slicePixelX + 1, -StripWidth/2, StripWidth/2), sliceCenterPixelY + FMath::Clamp(slicePixelY + 1, -StripHeight/2, StripHeight/2));

                        const FColor pixelColorTL = SurfaceData[atlasSampleTL.Y * UnprojectedAtlasWidth + atlasSampleTL.X];
                        const FColor pixelColorTR = SurfaceData[atlasSampleTR.Y * UnprojectedAtlasWidth + atlasSampleTR.X];
                        const FColor pixelColorBL = SurfaceData[atlasSampleBL.Y * UnprojectedAtlasWidth + atlasSampleBL.X];
                        const FColor pixelColorBR = SurfaceData[atlasSampleBR.Y * UnprojectedAtlasWidth + atlasSampleBR.X];

                        const float fracX = FMath::Frac(dbgMatchCaptureSliceFovToAtlasSliceFov ? sliceU * StripWidth : sliceU * CaptureWidth);
                        const float fracY = FMath::Frac(dbgMatchCaptureSliceFovToAtlasSliceFov ? sliceV * StripHeight : sliceV * CaptureHeight);

                        //Reinterpret as linear (a.k.a dont apply srgb inversion)
                        slicePixelSample = FMath::BiLerp(
                            pixelColorTL.ReinterpretAsLinear(), pixelColorTR.ReinterpretAsLinear(),
                            pixelColorBL.ReinterpretAsLinear(), pixelColorBR.ReinterpretAsLinear(),
                            fracX, fracY);
                    }
                    else
                    {
                        const int32 sliceCenterPixelX = (sliceXIndex + 0.5f) * StripWidth;
                        const int32 sliceCenterPixelY = (sliceYIndex + 0.5f) * StripHeight;

                        const int32 atlasSampleX = sliceCenterPixelX + slicePixelX;
                        const int32 atlasSampleY = sliceCenterPixelY + slicePixelY;


                        slicePixelSample = SurfaceData[atlasSampleY * UnprojectedAtlasWidth + atlasSampleX].ReinterpretAsLinear();
                    }

                    samplePixelAccum += slicePixelSample;

                    ////Output color map of projections
                    //const FColor debugEquiColors[12] = {
                    //    FColor(205, 180, 76),
                    //    FColor(190, 88, 202),
                    //    FColor(127, 185, 194),
                    //    FColor(90, 54, 47),
                    //    FColor(197, 88, 53),
                    //    FColor(197, 75, 124),
                    //    FColor(130, 208, 72),
                    //    FColor(136, 211, 153),
                    //    FColor(126, 130, 207),
                    //    FColor(83, 107, 59),
                    //    FColor(200, 160, 157),
                    //    FColor(80, 66, 106)
                    //};

                    //samplePixelAccum = ssPattern.numSamples * debugEquiColors[sliceYIndex * 4 + sliceXIndex];
                }

                SphericalAtlas[y * SphericalAtlasWidth + x] = (samplePixelAccum / ssPattern.numSamples).Quantize();

                // Force alpha value
                if (bForceAlpha)
                {
                    SphericalAtlas[y * SphericalAtlasWidth + x].A = 255;
                }
            }
        }

        //Blit the first column into the last column to make the stereo image seamless at theta=360
        for (int32 y = 0; y < SphericalAtlasHeight; y++)
        {
            SphericalAtlas[y * SphericalAtlasWidth + (SphericalAtlasWidth - 1)] = SphericalAtlas[y * SphericalAtlasWidth + 0];
        }

        const FTimespan SamplingDuration = FDateTime::UtcNow() - SamplingStartTime;
        UE_LOG(LogStereoPanorama, Log, TEXT("...done! Duration: %g seconds"), SamplingDuration.GetTotalSeconds());
    }
	
	// Generate name
	FString FrameString = FString::Printf( TEXT( "%s_%05d.png" ), *Folder, CurrentFrameCount );
    FString AtlasName =  OutputDir / Timestamp / FrameString;
    
	UE_LOG( LogStereoPanorama, Log, TEXT( "Writing atlas: %s" ), *AtlasName );

	// Write out PNG
    //TODO: ikrimae: Use threads to write out the images for performance
	IImageWrapperPtr ImageWrapper = ImageWrapperModule.CreateImageWrapper( EImageFormat::PNG );
    ImageWrapper->SetRaw(SphericalAtlas.GetData(), SphericalAtlas.GetAllocatedSize(), SphericalAtlasWidth, SphericalAtlasHeight, ERGBFormat::BGRA, 8);
	const TArray<uint8>& PNGData = ImageWrapper->GetCompressed(100);
	FFileHelper::SaveArrayToFile( PNGData, *AtlasName );

    if (FStereoPanoramaManager::GenerateDebugImages->GetInt() != 0)
    {
        FString FrameStringUnprojected = FString::Printf(TEXT("%s_%05d_Unprojected.png"), *Folder, CurrentFrameCount);
        FString AtlasNameUnprojected = OutputDir / Timestamp / FrameStringUnprojected;

        ImageWrapper->SetRaw(SurfaceData.GetData(), SurfaceData.GetAllocatedSize(), UnprojectedAtlasWidth, UnprojectedAtlasHeight, ERGBFormat::BGRA, 8);
        const TArray<uint8>& PNGDataUnprojected = ImageWrapper->GetCompressed(100);
        FFileHelper::SaveArrayToFile(PNGData, *AtlasNameUnprojected);
    }
	ImageWrapper.Reset();

	UE_LOG( LogStereoPanorama, Log, TEXT( " ... done!" ), *AtlasName );

    return SphericalAtlas;
}