GenerateAllMaterialScreenshots.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import azlmbr.bus
  7. import azlmbr.atomtools
  8. import azlmbr.materialeditor
  9. import azlmbr.name
  10. import azlmbr.render
  11. import azlmbr.paths
  12. import azlmbr.math
  13. import azlmbr.atom
  14. import azlmbr.asset
  15. import sys
  16. import os.path
  17. import filecmp
  18. g_engroot = azlmbr.paths.engroot
  19. sys.path.append(os.path.join(g_engroot, 'Tests', 'Atom', 'Automated'))
  20. g_materialTestFolder = os.path.join(g_engroot,'Gems','Atom','TestData','TestData','Materials','StandardPbrTestCases')
  21. # Change this to True to replace the expected screenshot images
  22. g_replaceExpectedScreenshots = False
  23. # This delay gives the material, model, and textures time to fully load before taking the screenshot
  24. # [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
  25. g_defaultDelayFrames = 10
  26. g_screenshotsTaken = 0
  27. # Track the number of material screenshots that are not identical for a report at the end
  28. g_materialsDontMatch = []
  29. # Track screenshots that were not checked for equality because either the actual or expected file didn't exist
  30. g_missingScreenshots = []
  31. # Track screenshots operations that failed
  32. g_failedScreenshots = []
  33. class ScreenshotHelper:
  34. """
  35. A helper to capture screenshots and wait for them.
  36. """
  37. def __init__(self, idle_wait_frames_callback):
  38. super().__init__()
  39. self.done = False
  40. self.capturedScreenshot = False
  41. self.max_frames_to_wait = 60
  42. self.idle_wait_frames_callback = idle_wait_frames_callback
  43. def capture_screenshot_blocking(self, filename):
  44. """
  45. Capture a screenshot and block the execution until the screenshot has been written to the disk.
  46. """
  47. self.done = False
  48. self.capturedScreenshot = False
  49. frameCaptureId = azlmbr.atom.FrameCaptureRequestBus(azlmbr.bus.Broadcast, "CaptureScreenshot", filename)
  50. if frameCaptureId != -1:
  51. self.handler = azlmbr.atom.FrameCaptureNotificationBusHandler()
  52. self.handler.connect(frameCaptureId)
  53. self.handler.add_callback('OnCaptureFinished', self.on_screenshot_captured)
  54. self.wait_until_screenshot()
  55. print("Screenshot taken.")
  56. else:
  57. print("screenshot failed")
  58. return self.capturedScreenshot
  59. def on_screenshot_captured(self, parameters):
  60. # the parameters come in as a tuple
  61. if parameters[0] == azlmbr.atom.FrameCaptureResult_Success:
  62. print("screenshot saved: {}".format(parameters[1]))
  63. self.capturedScreenshot = True
  64. else:
  65. print("screenshot failed: {}".format(parameters[1]))
  66. self.done = True
  67. self.handler.disconnect();
  68. def wait_until_screenshot(self):
  69. frames_waited = 0
  70. while self.done == False:
  71. self.idle_wait_frames_callback(1)
  72. if frames_waited > self.max_frames_to_wait:
  73. print("timeout while waiting for the screenshot to be written")
  74. self.handler.disconnect()
  75. break
  76. else:
  77. frames_waited = frames_waited + 1
  78. print("(waited {} frames)".format(frames_waited))
  79. def ToRadians(degrees):
  80. return 3.14159 * degrees / 180.0;
  81. def OpenMaterial(filename):
  82. documentId = azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(azlmbr.bus.Broadcast, 'OpenDocument', os.path.join(g_materialTestFolder, filename))
  83. return documentId
  84. def CloseMaterial(documentId):
  85. azlmbr.atomtools.AtomToolsDocumentSystemRequestBus(azlmbr.bus.Broadcast, 'CloseDocument', documentId)
  86. def LoadLightingPreset(path):
  87. assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', path, azlmbr.math.Uuid(), False)
  88. azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(azlmbr.bus.Broadcast, 'LoadLightingPresetByAssetId', assetId)
  89. def LoadModelPreset(path):
  90. assetId = azlmbr.asset.AssetCatalogRequestBus(azlmbr.bus.Broadcast, 'GetAssetIdByPath', path, azlmbr.math.Uuid(), False)
  91. azlmbr.atomtools.EntityPreviewViewportSettingsRequestBus(azlmbr.bus.Broadcast, 'LoadModelPresetByAssetId', assetId)
  92. def SetCameraDistance(distance):
  93. azlmbr.render.ArcBallControllerRequestBus(azlmbr.bus.Broadcast, 'SetDistance', distance)
  94. def SetCameraHeading(heading):
  95. azlmbr.render.ArcBallControllerRequestBus(azlmbr.bus.Broadcast, 'SetHeading', heading)
  96. def SetCameraPitch(pitch):
  97. azlmbr.render.ArcBallControllerRequestBus(azlmbr.bus.Broadcast, 'SetPitch', pitch)
  98. def IdleFrames(numFrames):
  99. azlmbr.atomtools.general.idle_wait_frames(numFrames)
  100. def CaptureScreenshot(screenshotOutputPath):
  101. print("Capturing screenshot to " + screenshotOutputPath + " ...")
  102. return ScreenshotHelper(azlmbr.atomtools.general.idle_wait_frames).capture_screenshot_blocking(screenshotOutputPath)
  103. def ResizeViewport(width, height):
  104. # This locks the size of the render target to the desired resolution
  105. azlmbr.atomtools.AtomToolsMainWindowRequestBus(azlmbr.bus.Broadcast, 'LockViewportRenderTargetSize', width, height)
  106. # This resizes the window to closely match the render target resolution so it doesn't appear stretched while the script is running
  107. azlmbr.atomtools.AtomToolsMainWindowRequestBus(azlmbr.bus.Broadcast, 'ResizeViewportRenderTarget', width, height)
  108. def ReleaseViewportResolutionLock():
  109. azlmbr.atomtools.AtomToolsMainWindowRequestBus(azlmbr.bus.Broadcast, 'UnlockViewportRenderTargetSize')
  110. def GenerateMaterialScreenshot(materialName,
  111. uniqueSuffix="",
  112. cameraHeading=-30.0,
  113. cameraPitch=20.0,
  114. cameraDistance=1.25,
  115. lighting="materialeditor/lightingpresets/neutral_urban.lightingpreset.azasset",
  116. model="materialeditor/viewportmodels/shaderball.modelpreset.azasset",
  117. delayFrames=g_defaultDelayFrames):
  118. """
  119. Opens a material, takes a screenshot in the material editor viewport, and saves the file to ppm.
  120. Also sets the camera position, lighting preset, and model for the screenshot.
  121. The screenshots will be saved in g_materialTestFolder.
  122. If g_replaceExpectedScreenshots is true, it will replace the baseline "expected" screenshots. Otherwise,
  123. the screenshots will be saved alongside the "expected" screenshots for comparison.
  124. @param materialName name of the material file to process, not including the path or ".material" extension.
  125. @param uniqueSuffix optional name for this particular screenshot configuration. Used to make a unique filename
  126. when taking multiple screenshots of the same material.
  127. @param cameraHeading heading of the camera in degrees.
  128. @param cameraPitch pitch of the camera in degrees.
  129. @param cameraDistance distance of the camera from the center point.
  130. @param lighting name of the lighting configuration to use.
  131. @param model name of the model configuration to use.
  132. @param delayFrames number of frames to delay before taking a screenshot, so the content has time to load.
  133. """
  134. print("GenerateMaterialScreenshot('{}', '{}')...".format(materialName, uniqueSuffix))
  135. global g_screenshotsTaken
  136. global g_materialsDontMatch
  137. global g_missingScreenshots
  138. global g_failedScreenshots
  139. documentId = OpenMaterial(materialName + '.material')
  140. LoadLightingPreset(lighting)
  141. LoadModelPreset(model)
  142. SetCameraDistance(cameraDistance)
  143. SetCameraHeading(ToRadians(cameraHeading))
  144. SetCameraPitch(ToRadians(-cameraPitch))
  145. IdleFrames(g_defaultDelayFrames); # The UI needs to time to process the changed scene data
  146. # This delay gives the material, model, and textures time to fully load before taking the screenshot
  147. # [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
  148. IdleFrames(delayFrames)
  149. screenshotsFolder = os.path.join(g_materialTestFolder, "Screenshots")
  150. uniqueFileName = materialName
  151. if len(uniqueSuffix) > 0:
  152. uniqueFileName = uniqueFileName + "." + uniqueSuffix
  153. # Note we use .ppm instead of .dds because more tools support it (especially BeyondCompare and ReviewBoard).
  154. expectedScreenshotPath = os.path.join(screenshotsFolder, uniqueFileName + ".expected.ppm")
  155. actualScreenshotPath = os.path.join(screenshotsFolder, uniqueFileName + ".actual.ppm")
  156. captureScreenshotPath = ""
  157. if g_replaceExpectedScreenshots:
  158. captureScreenshotPath = expectedScreenshotPath
  159. else:
  160. captureScreenshotPath = actualScreenshotPath
  161. screenshotSuccess = CaptureScreenshot(captureScreenshotPath)
  162. if screenshotSuccess:
  163. g_screenshotsTaken = g_screenshotsTaken + 1
  164. CloseMaterial(documentId)
  165. if not screenshotSuccess:
  166. g_failedScreenshots.append(captureScreenshotPath)
  167. elif not os.path.exists(expectedScreenshotPath):
  168. g_missingScreenshots.append(expectedScreenshotPath)
  169. elif not os.path.exists(actualScreenshotPath):
  170. g_missingScreenshots.append(actualScreenshotPath)
  171. elif not filecmp.cmp(expectedScreenshotPath, actualScreenshotPath):
  172. g_materialsDontMatch.append(actualScreenshotPath)
  173. def GenerateAllMaterialScreenshots():
  174. """
  175. Takes screenshots of a list of material files and saves them in g_replaceExpectedScreenshots
  176. """
  177. # First open any material to ensure the tab bar shows up before we resize the viewport. Otherwise the First
  178. # screenshot might have a different size from the others.
  179. OpenMaterial('001_DefaultWhite.material')
  180. # [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
  181. # ResizeViewportRenderTarget() support on both dx12 and vulkan. And QT can't resize to specific resolutions in device-pixel units (we can
  182. # achieve 999x999 and 1001x1001 but not 1000x1000 for example).
  183. ResizeViewport(999, 999)
  184. 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.
  185. GenerateMaterialScreenshot('001_DefaultWhite')
  186. GenerateMaterialScreenshot('002_BaseColorLerp')
  187. GenerateMaterialScreenshot('002_BaseColorLinearLight')
  188. GenerateMaterialScreenshot('002_BaseColorMultiply')
  189. GenerateMaterialScreenshot('003_MetalMatte')
  190. GenerateMaterialScreenshot('003_MetalPolished')
  191. GenerateMaterialScreenshot('004_MetalMap')
  192. GenerateMaterialScreenshot('005_RoughnessMap')
  193. GenerateMaterialScreenshot('006_SpecularF0Map')
  194. GenerateMaterialScreenshot('007_MultiscatteringCompensationOff')
  195. GenerateMaterialScreenshot('007_MultiscatteringCompensationOn')
  196. GenerateMaterialScreenshot('008_NormalMap')
  197. GenerateMaterialScreenshot('008_NormalMap_Bevels')
  198. GenerateMaterialScreenshot('009_Opacity_Blended', lighting="materialeditor/lightingpresets/neutral_urban.lightingpreset.azasset", model="materialeditor/viewportmodels/beveledcube.modelpreset.azasset")
  199. GenerateMaterialScreenshot('009_Opacity_Cutout_PackedAlpha_DoubleSided', lighting="materialeditor/lightingpresets/neutral_urban.lightingpreset.azasset", model="materialeditor/viewportmodels/beveledcube.modelpreset.azasset")
  200. GenerateMaterialScreenshot('009_Opacity_Cutout_SplitAlpha_DoubleSided', lighting="materialeditor/lightingpresets/neutral_urban.lightingpreset.azasset", model="materialeditor/viewportmodels/beveledcube.modelpreset.azasset")
  201. GenerateMaterialScreenshot('009_Opacity_Cutout_SplitAlpha_SingleSided', lighting="materialeditor/lightingpresets/neutral_urban.lightingpreset.azasset", model="materialeditor/viewportmodels/beveledcube.modelpreset.azasset")
  202. GenerateMaterialScreenshot('010_AmbientOcclusion')
  203. GenerateMaterialScreenshot('011_Emissive')
  204. GenerateMaterialScreenshot('012_Parallax_POM', model="materialeditor/viewportmodels/cube.modelpreset.azasset", cameraHeading=-35.0, cameraPitch=35.0)
  205. GenerateMaterialScreenshot('013_SpecularAA_Off', lighting="testdata/test.lightingpreset.azasset")
  206. GenerateMaterialScreenshot('013_SpecularAA_On', lighting="testdata/test.lightingpreset.azasset")
  207. GenerateMaterialScreenshot('100_UvTiling_AmbientOcclusion')
  208. GenerateMaterialScreenshot('100_UvTiling_BaseColor')
  209. GenerateMaterialScreenshot('100_UvTiling_Emissive')
  210. GenerateMaterialScreenshot('100_UvTiling_Metallic')
  211. GenerateMaterialScreenshot('100_UvTiling_Normal')
  212. GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_Rotate20', model="materialeditor/viewportmodels/cube.modelpreset.azasset", lighting="testdata/test.lightingpreset.azasset", cameraHeading=225.0)
  213. GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_Rotate90', model="materialeditor/viewportmodels/cube.modelpreset.azasset", lighting="testdata/test.lightingpreset.azasset", cameraHeading=225.0)
  214. GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_ScaleOnlyU', model="materialeditor/viewportmodels/cube.modelpreset.azasset", lighting="testdata/test.lightingpreset.azasset", cameraHeading=225.0)
  215. GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_ScaleOnlyV', model="materialeditor/viewportmodels/cube.modelpreset.azasset", lighting="testdata/test.lightingpreset.azasset", cameraHeading=225.0)
  216. GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_ScaleUniform', model="materialeditor/viewportmodels/cube.modelpreset.azasset", lighting="testdata/test.lightingpreset.azasset", cameraHeading=225.0)
  217. GenerateMaterialScreenshot('100_UvTiling_Normal_Dome_TransformAll', model="materialeditor/viewportmodels/cube.modelpreset.azasset", lighting="testdata/test.lightingpreset.azasset", cameraHeading=225.0)
  218. GenerateMaterialScreenshot('100_UvTiling_Opacity', lighting="materialeditor/lightingpresets/neutral_urban.lightingpreset.azasset")
  219. GenerateMaterialScreenshot('100_UvTiling_Parallax_A', uniqueSuffix="Angle1", model="materialeditor/viewportmodels/cube.modelpreset.azasset", cameraHeading=35.0, cameraPitch=35.0)
  220. GenerateMaterialScreenshot('100_UvTiling_Parallax_A', uniqueSuffix="Angle2", model="materialeditor/viewportmodels/cube.modelpreset.azasset", cameraHeading=125.0, cameraPitch=35.0)
  221. GenerateMaterialScreenshot('100_UvTiling_Parallax_B', uniqueSuffix="Angle1", model="materialeditor/viewportmodels/cube.modelpreset.azasset", cameraHeading=0.0, cameraPitch=45.0, cameraDistance=1.0)
  222. GenerateMaterialScreenshot('100_UvTiling_Parallax_B', uniqueSuffix="Angle2", model="materialeditor/viewportmodels/cube.modelpreset.azasset", cameraHeading=90.0, cameraPitch=45.0, cameraDistance=1.0)
  223. GenerateMaterialScreenshot('100_UvTiling_Roughness')
  224. GenerateMaterialScreenshot('100_UvTiling_SpecularF0')
  225. ReleaseViewportResolutionLock()
  226. def main():
  227. global g_screenshotsTaken
  228. global g_materialsDontMatch
  229. global g_missingScreenshots
  230. global g_failedScreenshots
  231. g_screenshotsTaken = 0
  232. g_materialsDontMatch = []
  233. g_missingScreenshots = []
  234. g_failedScreenshots = []
  235. print("==== Begin screenshot script ==========================================================")
  236. GenerateAllMaterialScreenshots()
  237. print("==== Summary Report ===================================================================")
  238. print("Screenshots taken: {}".format(g_screenshotsTaken))
  239. print("Screenshots failed: {}".format(len(g_failedScreenshots)))
  240. print(g_failedScreenshots)
  241. print("Missing screenshots: {}".format(len(g_missingScreenshots)))
  242. print(g_missingScreenshots)
  243. 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.)")
  244. print("Mismatched screenshots: {}".format(len(g_materialsDontMatch)))
  245. print(g_materialsDontMatch)
  246. print("==== End screenshot script ============================================================")
  247. if __name__ == "__main__":
  248. main()