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.
300 lines
14 KiB
Python
300 lines
14 KiB
Python
"""
|
|
Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
|
|
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()
|
|
|