You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Gems/Atom/Tools/MaterialEditor/Scripts/GenerateAllMaterialScreensh...

300 lines
14 KiB
Python

"""
Copyright (c) Contributors to the Open 3D Engine Project
SPDX-License-Identifier: Apache-2.0 OR MIT
"""
import azlmbr.bus
import azlmbr.materialeditor
import azlmbr.name
import azlmbr.render
import azlmbr.paths
import azlmbr.atom
import sys
import os.path
import filecmp
g_devroot = azlmbr.paths.devroot
sys.path.append(os.path.join(g_devroot, 'Tests', 'Atom', 'Automated'))
g_materialTestFolder = os.path.join(g_devroot,'Gems','Atom','TestData','TestData','Materials','StandardPbrTestCases')
# Change this to True to replace the expected screenshot images
g_replaceExpectedScreenshots = False
# This delay gives the material, model, and textures time to fully load before taking the screenshot
# [GFX TODO][ATOM-4819] Replace this with a callback mechanism that will allow the test to continue as soon as the content is fully loaded
g_defaultDelayFrames = 10
g_screenshotsTaken = 0
# Track the number of material screenshots that are not identical for a report at the end
g_materialsDontMatch = []
# Track screenshots that were not checked for equality because either the actual or expected file didn't exist
g_missingScreenshots = []
# Track screenshots operations that failed
g_failedScreenshots = []
class ScreenshotHelper:
"""
A helper to capture screenshots and wait for them.
"""
def __init__(self, idle_wait_frames_callback):
super().__init__()
self.done = False
self.capturedScreenshot = False
self.max_frames_to_wait = 60
self.idle_wait_frames_callback = idle_wait_frames_callback
def capture_screenshot_blocking(self, filename):
"""
Capture a screenshot and block the execution until the screenshot has been written to the disk.
"""
self.handler = azlmbr.atom.FrameCaptureNotificationBusHandler()
self.handler.connect()
self.handler.add_callback('OnCaptureFinished', self.on_screenshot_captured)
self.done = False
self.capturedScreenshot = False
success = azlmbr.atom.FrameCaptureRequestBus(azlmbr.bus.Broadcast, "CaptureScreenshot", filename)
if success:
self.wait_until_screenshot()
print("Screenshot taken.")
else:
print("screenshot failed")
return self.capturedScreenshot
def on_screenshot_captured(self, parameters):
# the parameters come in as a tuple
if parameters[0] == azlmbr.atom.FrameCaptureResult_Success:
print("screenshot saved: {}".format(parameters[1]))
self.capturedScreenshot = True
else:
print("screenshot failed: {}".format(parameters[1]))
self.done = True
self.handler.disconnect();
def wait_until_screenshot(self):
frames_waited = 0
while self.done == False:
self.idle_wait_frames_callback(1)
if frames_waited > self.max_frames_to_wait:
print("timeout while waiting for the screenshot to be written")
self.handler.disconnect()
break
else:
frames_waited = frames_waited + 1
print("(waited {} frames)".format(frames_waited))
def ToRadians(degrees):
return 3.14159 * degrees / 180.0;
def OpenMaterial(filename):
documentId = azlmbr.materialeditor.MaterialDocumentSystemRequestBus(azlmbr.bus.Broadcast, 'OpenDocument', os.path.join(g_materialTestFolder, filename))
return documentId
def CloseMaterial(documentId):
azlmbr.materialeditor.MaterialDocumentSystemRequestBus(azlmbr.bus.Broadcast, 'CloseDocument', documentId)
def SelectLightingPreset(presetName):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, 'SelectLightingPresetByName', presetName)
def SelectModelPreset(presetName):
azlmbr.materialeditor.MaterialViewportRequestBus(azlmbr.bus.Broadcast, 'SelectModelPresetByName', presetName)
def SetCameraDistance(distance):
azlmbr.render.ArcBallControllerRequestBus(azlmbr.bus.Broadcast, 'SetDistance', distance)
def SetCameraHeading(heading):
azlmbr.render.ArcBallControllerRequestBus(azlmbr.bus.Broadcast, 'SetHeading', heading)
def SetCameraPitch(pitch):
azlmbr.render.ArcBallControllerRequestBus(azlmbr.bus.Broadcast, 'SetPitch', pitch)
def IdleFrames(numFrames):
azlmbr.materialeditor.general.idle_wait_frames(numFrames)
def CaptureScreenshot(screenshotOutputPath):
print("Capturing screenshot to " + screenshotOutputPath + " ...")
return ScreenshotHelper(azlmbr.materialeditor.general.idle_wait_frames).capture_screenshot_blocking(screenshotOutputPath)
def ResizeViewport(width, height):
# This locks the size of the render target to the desired resolution
azlmbr.materialeditor.MaterialEditorWindowRequestBus(azlmbr.bus.Broadcast, 'LockViewportRenderTargetSize', width, height)
# This resizes the window to closely match the render target resolution so it doesn't appear stretched while the script is running
azlmbr.materialeditor.MaterialEditorWindowRequestBus(azlmbr.bus.Broadcast, 'ResizeViewportRenderTarget', width, height)
def ReleaseViewportResolutionLock():
azlmbr.materialeditor.MaterialEditorWindowRequestBus(azlmbr.bus.Broadcast, 'UnlockViewportRenderTargetSize')
def GenerateMaterialScreenshot(materialName,
uniqueSuffix="",
cameraHeading=-30.0,
cameraPitch=20.0,
cameraDistance=1.25,
lighting="Neutral Urban (Alt)",
model="Shader Ball",
delayFrames=g_defaultDelayFrames):
"""
Opens a material, takes a screenshot in the material editor viewport, and saves the file to ppm.
Also sets the camera position, lighting preset, and model for the screenshot.
The screenshots will be saved in g_materialTestFolder.
If g_replaceExpectedScreenshots is true, it will replace the baseline "expected" screenshots. Otherwise,
the screenshots will be saved alongside the "expected" screenshots for comparison.
@param materialName name of the material file to process, not including the path or ".material" extension.
@param uniqueSuffix optional name for this particular screenshot configuration. Used to make a unique filename
when taking multiple screenshots of the same material.
@param cameraHeading heading of the camera in degrees.
@param cameraPitch pitch of the camera in degrees.
@param cameraDistance distance of the camera from the center point.
@param lighting name of the lighting configuration to use.
@param model name of the model configuration to use.
@param delayFrames number of frames to delay before taking a screenshot, so the content has time to load.
"""
print("GenerateMaterialScreenshot('{}', '{}')...".format(materialName, uniqueSuffix))
global g_screenshotsTaken
global g_materialsDontMatch
global g_missingScreenshots
global g_failedScreenshots
documentId = OpenMaterial(materialName + '.material')
SelectLightingPreset(lighting)
SelectModelPreset(model)
SetCameraDistance(cameraDistance)
SetCameraHeading(ToRadians(cameraHeading))
SetCameraPitch(ToRadians(-cameraPitch))
IdleFrames(g_defaultDelayFrames); # The UI needs to time to process the changed scene data
# This delay gives the material, model, and textures time to fully load before taking the screenshot
# [GFX TODO][ATOM-4819] Replace this with a callback mechanism that will allow the test to continue as soon as the content is fully loaded
IdleFrames(delayFrames)
screenshotsFolder = os.path.join(g_materialTestFolder, "Screenshots")
uniqueFileName = materialName
if len(uniqueSuffix) > 0:
uniqueFileName = uniqueFileName + "." + uniqueSuffix
# Note we use .ppm instead of .dds because more tools support it (especially BeyondCompare and ReviewBoard).
expectedScreenshotPath = os.path.join(screenshotsFolder, uniqueFileName + ".expected.ppm")
actualScreenshotPath = os.path.join(screenshotsFolder, uniqueFileName + ".actual.ppm")
captureScreenshotPath = ""
if g_replaceExpectedScreenshots:
captureScreenshotPath = expectedScreenshotPath
else:
captureScreenshotPath = actualScreenshotPath
screenshotSuccess = CaptureScreenshot(captureScreenshotPath)
if screenshotSuccess:
g_screenshotsTaken = g_screenshotsTaken + 1
CloseMaterial(documentId)
if not screenshotSuccess:
g_failedScreenshots.append(captureScreenshotPath)
elif not os.path.exists(expectedScreenshotPath):
g_missingScreenshots.append(expectedScreenshotPath)
elif not os.path.exists(actualScreenshotPath):
g_missingScreenshots.append(actualScreenshotPath)
elif not filecmp.cmp(expectedScreenshotPath, actualScreenshotPath):
g_materialsDontMatch.append(actualScreenshotPath)
def GenerateAllMaterialScreenshots():
"""
Takes screenshots of a list of material files and saves them in g_replaceExpectedScreenshots
"""
# First open any material to ensure the tab bar shows up before we resize the viewport. Otherwise the First
# screenshot might have a different size from the others.
OpenMaterial('001_DefaultWhite.material')
# [GFX TODO][ATOM-4909] We have to use the strange viewport size because of limitations in both the RPI and QT. The RPI doesn't provide
# ResizeViewportRenderTarget() support on both dx12 and vulkan. And QT can't resize to specific resolutions in device-pixel units (we can
# achieve 999x999 and 1001x1001 but not 1000x1000 for example).
ResizeViewport(999, 999)
IdleFrames(g_defaultDelayFrames); # Allows the UI to refresh before continuing; otherwise the viewport will appear stretched while the user waits a second for the screen capture.
GenerateMaterialScreenshot('001_DefaultWhite')
GenerateMaterialScreenshot('002_BaseColorLerp')
GenerateMaterialScreenshot('002_BaseColorLinearLight')
GenerateMaterialScreenshot('002_BaseColorMultiply')
GenerateMaterialScreenshot('003_MetalMatte')
GenerateMaterialScreenshot('003_MetalPolished')
GenerateMaterialScreenshot('004_MetalMap')
GenerateMaterialScreenshot('005_RoughnessMap')
GenerateMaterialScreenshot('006_SpecularF0Map')
GenerateMaterialScreenshot('007_MultiscatteringCompensationOff')
GenerateMaterialScreenshot('007_MultiscatteringCompensationOn')
GenerateMaterialScreenshot('008_NormalMap')
GenerateMaterialScreenshot('008_NormalMap_Bevels')
GenerateMaterialScreenshot('009_Opacity_Blended', lighting="Neutral Urban", model="Cube (Beveled)")
GenerateMaterialScreenshot('009_Opacity_Cutout_PackedAlpha_DoubleSided', lighting="Neutral Urban", model="Cube (Beveled)")
GenerateMaterialScreenshot('009_Opacity_Cutout_SplitAlpha_DoubleSided', lighting="Neutral Urban", model="Cube (Beveled)")
GenerateMaterialScreenshot('009_Opacity_Cutout_SplitAlpha_SingleSided', lighting="Neutral Urban", model="Cube (Beveled)")
GenerateMaterialScreenshot('010_AmbientOcclusion')
GenerateMaterialScreenshot('011_Emissive')
GenerateMaterialScreenshot('012_Parallax_POM', model="Cube", cameraHeading=-35.0, cameraPitch=35.0)
GenerateMaterialScreenshot('013_SpecularAA_Off', lighting="Dark Test Lighting")
GenerateMaterialScreenshot('013_SpecularAA_On', lighting="Dark Test Lighting")
GenerateMaterialScreenshot('100_UvTiling_AmbientOcclusion')
GenerateMaterialScreenshot('100_UvTiling_BaseColor')
GenerateMaterialScreenshot('100_UvTiling_Emissive')
GenerateMaterialScreenshot('100_UvTiling_Metallic')
GenerateMaterialScreenshot('100_UvTiling_Normal')
GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_Rotate20', model="Cube", lighting="Dark Test Lighting", cameraHeading=225.0)
GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_Rotate90', model="Cube", lighting="Dark Test Lighting", cameraHeading=225.0)
GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_ScaleOnlyU', model="Cube", lighting="Dark Test Lighting", cameraHeading=225.0)
GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_ScaleOnlyV', model="Cube", lighting="Dark Test Lighting", cameraHeading=225.0)
GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_ScaleUniform', model="Cube", lighting="Dark Test Lighting", cameraHeading=225.0)
GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_TransformAll', model="Cube", lighting="Dark Test Lighting", cameraHeading=225.0)
GenerateMaterialScreenshot('100_UvTiling_Opacity', lighting="Neutral Urban")
GenerateMaterialScreenshot('100_UvTiling_Parallax_A', uniqueSuffix="Angle1", model="Cube", cameraHeading=35.0, cameraPitch=35.0)
GenerateMaterialScreenshot('100_UvTiling_Parallax_A', uniqueSuffix="Angle2", model="Cube", cameraHeading=125.0, cameraPitch=35.0)
GenerateMaterialScreenshot('100_UvTiling_Parallax_B', uniqueSuffix="Angle1", model="Cube", cameraHeading=0.0, cameraPitch=45.0, cameraDistance=1.0)
GenerateMaterialScreenshot('100_UvTiling_Parallax_B', uniqueSuffix="Angle2", model="Cube", cameraHeading=90.0, cameraPitch=45.0, cameraDistance=1.0)
GenerateMaterialScreenshot('100_UvTiling_Roughness')
GenerateMaterialScreenshot('100_UvTiling_SpecularF0')
ReleaseViewportResolutionLock()
def main():
global g_screenshotsTaken
global g_materialsDontMatch
global g_missingScreenshots
global g_failedScreenshots
g_screenshotsTaken = 0
g_materialsDontMatch = []
g_missingScreenshots = []
g_failedScreenshots = []
print("==== Begin screenshot script ==========================================================")
GenerateAllMaterialScreenshots()
print("==== Summary Report ===================================================================")
print("Screenshots taken: {}".format(g_screenshotsTaken))
print("Screenshots failed: {}".format(len(g_failedScreenshots)))
print(g_failedScreenshots)
print("Missing screenshots: {}".format(len(g_missingScreenshots)))
print(g_missingScreenshots)
print("\n(The following stats are for informational purposes. A mismatched file doesn't necessarily mean a test failed. Mismatched files will need to be image-diffed in another tool.)")
print("Mismatched screenshots: {}".format(len(g_materialsDontMatch)))
print(g_materialsDontMatch)
print("==== End screenshot script ============================================================")
if __name__ == "__main__":
main()