import { Nullable } from "../types"; import { Camera } from "../Cameras/camera"; import { Texture } from "../Materials/Textures/texture"; import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture"; import { FxaaPostProcess } from "../PostProcesses/fxaaPostProcess"; import { Constants } from "../Engines/constants"; import { Logger } from "./logger"; import { _TypeStore } from "./typeStore"; import { Tools } from "./tools"; import { IScreenshotSize } from './interfaces/screenshotSize'; declare type Engine = import("../Engines/engine").Engine; /** * Class containing a set of static utilities functions for screenshots */ export class ScreenshotTools { /** * Captures a screenshot of the current rendering * @see https://doc.babylonjs.com/how_to/render_scene_on_a_png * @param engine defines the rendering engine * @param camera defines the source camera * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param successCallback defines the callback receives a single parameter which contains the * screenshot as a string of base64-encoded characters. This string can be assigned to the * src parameter of an to display it * @param mimeType defines the MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types */ public static CreateScreenshot(engine: Engine, camera: Camera, size: IScreenshotSize | number, successCallback?: (data: string) => void, mimeType: string = "image/png"): void { const { height, width } = ScreenshotTools._getScreenshotSize(engine, camera, size); if (!(height && width)) { Logger.Error("Invalid 'size' parameter !"); return; } if (!Tools._ScreenshotCanvas) { Tools._ScreenshotCanvas = document.createElement('canvas'); } Tools._ScreenshotCanvas.width = width; Tools._ScreenshotCanvas.height = height; var renderContext = Tools._ScreenshotCanvas.getContext("2d"); var ratio = engine.getRenderWidth() / engine.getRenderHeight(); var newWidth = width; var newHeight = newWidth / ratio; if (newHeight > height) { newHeight = height; newWidth = newHeight * ratio; } var offsetX = Math.max(0, width - newWidth) / 2; var offsetY = Math.max(0, height - newHeight) / 2; engine.onEndFrameObservable.addOnce(() => { var renderingCanvas = engine.getRenderingCanvas(); if (renderContext && renderingCanvas) { renderContext.drawImage(renderingCanvas, offsetX, offsetY, newWidth, newHeight); } Tools.EncodeScreenshotCanvasData(successCallback, mimeType); }); } /** * Captures a screenshot of the current rendering * @see https://doc.babylonjs.com/how_to/render_scene_on_a_png * @param engine defines the rendering engine * @param camera defines the source camera * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param mimeType defines the MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @returns screenshot as a string of base64-encoded characters. This string can be assigned * to the src parameter of an to display it */ public static CreateScreenshotAsync(engine: Engine, camera: Camera, size: any, mimeType: string = "image/png"): Promise { return new Promise((resolve, reject) => { ScreenshotTools.CreateScreenshot(engine, camera, size, (data) => { if (typeof(data) !== "undefined") { resolve(data); } else { reject(new Error("Data is undefined")); } }, mimeType); }); } /** * Generates an image screenshot from the specified camera. * @see https://doc.babylonjs.com/how_to/render_scene_on_a_png * @param engine The engine to use for rendering * @param camera The camera to use for rendering * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param successCallback The callback receives a single parameter which contains the * screenshot as a string of base64-encoded characters. This string can be assigned to the * src parameter of an to display it * @param mimeType The MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @param samples Texture samples (default: 1) * @param antialiasing Whether antialiasing should be turned on or not (default: false) * @param fileName A name for for the downloaded file. * @param renderSprites Whether the sprites should be rendered or not (default: false) * @param enableStencilBuffer Whether the stencil buffer should be enabled or not (default: false) */ public static CreateScreenshotUsingRenderTarget(engine: Engine, camera: Camera, size: IScreenshotSize | number, successCallback?: (data: string) => void, mimeType: string = "image/png", samples: number = 1, antialiasing: boolean = false, fileName?: string, renderSprites: boolean = false, enableStencilBuffer: boolean = false): void { const { height, width } = ScreenshotTools._getScreenshotSize(engine, camera, size); let targetTextureSize = { width, height }; if (!(height && width)) { Logger.Error("Invalid 'size' parameter !"); return; } var scene = camera.getScene(); var previousCamera: Nullable = null; if (scene.activeCamera !== camera) { previousCamera = scene.activeCamera; scene.activeCamera = camera; } // At this point size can be a number, or an object (according to engine.prototype.createRenderTargetTexture method) var texture = new RenderTargetTexture("screenShot", targetTextureSize, scene, false, false, Constants.TEXTURETYPE_UNSIGNED_INT, false, Texture.NEAREST_SAMPLINGMODE, undefined, enableStencilBuffer, undefined, undefined, undefined, samples); texture.renderList = null; texture.samples = samples; texture.renderSprites = renderSprites; engine.onEndFrameObservable.addOnce(() => { texture.readPixels()!.then((data) => { Tools.DumpData(width, height, data, successCallback, mimeType, fileName, true); texture.dispose(); if (previousCamera) { scene.activeCamera = previousCamera; } camera.getProjectionMatrix(true); // Force cache refresh; }); }); const renderToTexture = () => { scene.incrementRenderId(); scene.resetCachedMaterial(); texture.render(true); }; if (antialiasing) { const fxaaPostProcess = new FxaaPostProcess('antialiasing', 1.0, scene.activeCamera); texture.addPostProcess(fxaaPostProcess); // Async Shader Compilation can lead to none ready effects in synchronous code if (!fxaaPostProcess.getEffect().isReady()) { fxaaPostProcess.getEffect().onCompiled = () => { renderToTexture(); }; } // The effect is ready we can render else { renderToTexture(); } } else { // No need to wait for extra resources to be ready renderToTexture(); } } /** * Generates an image screenshot from the specified camera. * @see https://doc.babylonjs.com/how_to/render_scene_on_a_png * @param engine The engine to use for rendering * @param camera The camera to use for rendering * @param size This parameter can be set to a single number or to an object with the * following (optional) properties: precision, width, height. If a single number is passed, * it will be used for both width and height. If an object is passed, the screenshot size * will be derived from the parameters. The precision property is a multiplier allowing * rendering at a higher or lower resolution * @param mimeType The MIME type of the screenshot image (default: image/png). * Check your browser for supported MIME types * @param samples Texture samples (default: 1) * @param antialiasing Whether antialiasing should be turned on or not (default: false) * @param fileName A name for for the downloaded file. * @param renderSprites Whether the sprites should be rendered or not (default: false) * @returns screenshot as a string of base64-encoded characters. This string can be assigned * to the src parameter of an to display it */ public static CreateScreenshotUsingRenderTargetAsync(engine: Engine, camera: Camera, size: any, mimeType: string = "image/png", samples: number = 1, antialiasing: boolean = false, fileName?: string, renderSprites: boolean = false): Promise { return new Promise((resolve, reject) => { ScreenshotTools.CreateScreenshotUsingRenderTarget(engine, camera, size, (data) => { if (typeof(data) !== "undefined") { resolve(data); } else { reject(new Error("Data is undefined")); } }, mimeType, samples, antialiasing, fileName, renderSprites); }); } /** * Gets height and width for screenshot size * @private */ private static _getScreenshotSize(engine: Engine, camera: Camera, size: IScreenshotSize | number): {height: number, width: number} { let height = 0; let width = 0; //If a size value defined as object if (typeof(size) === 'object') { const precision = size.precision ? Math.abs(size.precision) // prevent GL_INVALID_VALUE : glViewport: negative width/height : 1; //If a width and height values is specified if (size.width && size.height) { height = size.height * precision; width = size.width * precision; } //If passing only width, computing height to keep display canvas ratio. else if (size.width && !size.height) { width = size.width * precision; height = Math.round(width / engine.getAspectRatio(camera)); } //If passing only height, computing width to keep display canvas ratio. else if (size.height && !size.width) { height = size.height * precision; width = Math.round(height * engine.getAspectRatio(camera)); } else { width = Math.round(engine.getRenderWidth() * precision); height = Math.round(width / engine.getAspectRatio(camera)); } } //Assuming here that "size" parameter is a number else if (!isNaN(size)) { height = size; width = size; } // When creating the image data from the CanvasRenderingContext2D, the width and height is clamped to the size of the _gl context // On certain GPUs, it seems as if the _gl context truncates to an integer automatically. Therefore, if a user tries to pass the width of their canvas element // and it happens to be a float (1000.5 x 600.5 px), the engine.readPixels will return a different size array than context.createImageData // to resolve this, we truncate the floats here to ensure the same size if (width) { width = Math.floor(width); } if (height) { height = Math.floor(height); } return { height: height | 0, width: width | 0 }; } } Tools.CreateScreenshot = ScreenshotTools.CreateScreenshot; Tools.CreateScreenshotAsync = ScreenshotTools.CreateScreenshotAsync; Tools.CreateScreenshotUsingRenderTarget = ScreenshotTools.CreateScreenshotUsingRenderTarget; Tools.CreateScreenshotUsingRenderTargetAsync = ScreenshotTools.CreateScreenshotUsingRenderTargetAsync;