import { Color } from '../../../../color/Color';
import { loadEnvironmentExtensions } from '../../../../environment/autoDetectEnvironment';
import { Container } from '../../../../scene/container/Container';
import { unsafeEvalSupported } from '../../../../utils/browser/unsafeEvalSupported';
import { deprecation, v8_0_0 } from '../../../../utils/logging/deprecation';
import { EventEmitter } from '../../../../utils/utils';
import { CLEAR } from '../../gl/const';
import { SystemRunner } from './SystemRunner';
import type { ColorSource, RgbaArray } from '../../../../color/Color';
import type { ICanvas } from '../../../../environment/canvas/ICanvas';
import type { Matrix } from '../../../../maths/matrix/Matrix';
import type { Rectangle } from '../../../../maths/shapes/Rectangle';
import type { TypeOrBool } from '../../../../scene/container/destroyTypes';
import type { CLEAR_OR_BOOL } from '../../gl/const';
import type { Renderer } from '../../types';
import type { BackgroundSystem } from '../background/BackgroundSystem';
import type { GenerateTextureOptions, GenerateTextureSystem } from '../extract/GenerateTextureSystem';
import type { PipeConstructor } from '../instructions/RenderPipe';
import type { RenderSurface } from '../renderTarget/RenderTargetSystem';
import type { Texture } from '../texture/Texture';
import type { ViewSystem, ViewSystemDestroyOptions } from '../view/ViewSystem';
import type { SharedRendererOptions } from './SharedSystems';
import type { System, SystemConstructor } from './System';
export interface RendererConfig
{
type: number;
name: string;
runners?: string[];
systems: {name: string, value: SystemConstructor}[];
renderPipes: {name: string, value: PipeConstructor}[];
renderPipeAdaptors: {name: string, value: any}[];
}
/**
* The options for rendering a view.
* @memberof rendering
*/
export interface RenderOptions extends ClearOptions
{
/** The container to render. */
container: Container;
/** the transform to apply to the container. */
transform?: Matrix;
}
/**
* The options for clearing the render target.
* @memberof rendering
*/
export interface ClearOptions
{
/** The render target to render. */
target?: RenderSurface;
/** The color to clear with. */
clearColor?: ColorSource;
/** The clear mode to use. */
clear?: CLEAR_OR_BOOL
}
export type RendererDestroyOptions = TypeOrBool<ViewSystemDestroyOptions>;
const defaultRunners = [
'init',
'destroy',
'contextChange',
'resolutionChange',
'reset',
'renderEnd',
'renderStart',
'render',
'update',
'postrender',
'prerender'
] as const;
type DefaultRunners = typeof defaultRunners[number];
type Runners = {[key in DefaultRunners]: SystemRunner} & {
// eslint-disable-next-line @typescript-eslint/ban-types
[K: ({} & string) | ({} & symbol)]: SystemRunner;
};
/* eslint-disable max-len */
/**
* The base class for a PixiJS Renderer. It contains the shared logic for all renderers.
*
* You should not use this class directly, but instead use {@linkrendering.WebGLRenderer}
* or WebGPURenderer.
* Alternatively, you can also use autoDetectRenderer if you want us to
* determine the best renderer for you.
*
* The renderer is composed of systems that manage specific tasks. The following systems are added by default
* whenever you create a renderer:
*
*
* | Generic Systems | Systems that manage functionality that all renderer types share |
* | ------------------------------------ | ----------------------------------------------------------------------------- |
* | ViewSystem | This manages the main view of the renderer usually a Canvas |
* | BackgroundSystem | This manages the main views background color and alpha |
* | EventSystem | This manages UI events. |
* | AccessibilitySystem | This manages accessibility features. Requires `import 'pixi.js/accessibility'`|
*
* | Core Systems | Provide an optimised, easy to use API to work with WebGL/WebGPU |
* | ------------------------------------ | ----------------------------------------------------------------------------- |
* | RenderGroupSystem | This manages the what what we are rendering to (eg - canvas or texture) |
* | GlobalUniformSystem | This manages shaders, programs that run on the GPU to calculate 'em pixels. |
* | TextureGCSystem | This will automatically remove textures from the GPU if they are not used. |
*
* | PixiJS High-Level Systems | Set of specific systems designed to work with PixiJS objects |
* | ------------------------------------ | ----------------------------------------------------------------------------- |
* | HelloSystem | Says hello, buy printing out the pixi version into the console log (along with the renderer type) |
* | GenerateTextureSystem | This adds the ability to generate textures from any Container |
* | FilterSystem | This manages the filtering pipeline for post-processing effects. |
* | PrepareSystem | This manages uploading assets to the GPU. Requires `import 'pixi.js/prepare'`|
* | ExtractSystem | This extracts image data from display objects. |
*
* The breadth of the API surface provided by the renderer is contained within these systems.
* @abstract
* @memberof rendering
* @property {rendering.HelloSystem} hello - HelloSystem instance.
* @property {rendering.RenderGroupSystem} renderGroup - RenderGroupSystem instance.
* @property {rendering.TextureGCSystem} textureGC - TextureGCSystem instance.
* @property {rendering.FilterSystem} filter - FilterSystem instance.
* @property {rendering.GlobalUniformSystem} globalUniforms - GlobalUniformSystem instance.
* @property {rendering.TextureSystem} texture - TextureSystem instance.
* @property {rendering.EventSystem} events - EventSystem instance.
* @property {rendering.ExtractSystem} extract - ExtractSystem instance. Requires `import 'pixi.js/extract'`.
* @property {rendering.PrepareSystem} prepare - PrepareSystem instance. Requires `import 'pixi.js/prepare'`.
* @property {rendering.AccessibilitySystem} accessibility - AccessibilitySystem instance. Requires `import 'pixi.js/accessibility'`.
*/
/* eslint-enable max-len */
export class AbstractRenderer<
PIPES, OPTIONS extends SharedRendererOptions, CANVAS extends ICanvas = HTMLCanvasElement
> extends EventEmitter<{resize: [number, number]}>
{
/** The default options for the renderer. */
public static defaultOptions = {
/**
* Default resolution / device pixel ratio of the renderer.
* @default 1
*/
resolution: 1,
/**
* Should the `failIfMajorPerformanceCaveat` flag be enabled as a context option used in the `isWebGLSupported`
* function. If set to true, a WebGL renderer can fail to be created if the browser thinks there could be
* performance issues when using WebGL.
*
* In PixiJS v6 this has changed from true to false by default, to allow WebGL to work in as many
* scenarios as possible. However, some users may have a poor experience, for example, if a user has a gpu or
* driver version blacklisted by the
* browser.
*
* If your application requires high performance rendering, you may wish to set this to false.
* We recommend one of two options if you decide to set this flag to false:
*
* 1: Use the Canvas renderer as a fallback in case high performance WebGL is
* not supported.
*
* 2: Call `isWebGLSupported` (which if found in the utils package) in your code before attempting to create a
* PixiJS renderer, and show an error message to the user if the function returns false, explaining that their
* device & browser combination does not support high performance WebGL.
* This is a much better strategy than trying to create a PixiJS renderer and finding it then fails.
* @default false
*/
failIfMajorPerformanceCaveat: false,
/**
* Should round pixels be forced when rendering?
* @default false
*/
roundPixels: false
};
public readonly type: number;
/** The name of the renderer. */
public readonly name: string;
public _roundPixels: 0 | 1;
public readonly runners: Runners = Object.create(null) as Runners;
public readonly renderPipes = Object.create(null) as PIPES;
/** The view system manages the main canvas that is attached to the DOM */
public view!: ViewSystem;
/** The background system manages the background color and alpha of the main view. */
public background: BackgroundSystem;
/** System that manages the generation of textures from the renderer */
public textureGenerator: GenerateTextureSystem;
protected _initOptions: OPTIONS = {} as OPTIONS;
protected config: RendererConfig;
private _systemsHash: Record<string, System> = Object.create(null);
private _lastObjectRendered: Container;
/**
* Set up a system with a collection of SystemClasses and runners.
* Systems are attached dynamically to this class when added.
* @param config - the config for the system manager
*/
constructor(config: RendererConfig)
{
super();
this.type = config.type;
this.name = config.name;
this.config = config;
const combinedRunners = [...defaultRunners, ...(this.config.runners ?? [])];
this._addRunners(...combinedRunners);
// Validation check that this environment support `new Function`
this._unsafeEvalCheck();
}
/**
* Initialize the renderer.
* @param options - The options to use to create the renderer.
*/
public async init(options: Partial<OPTIONS> = {})
{
const skip = options.skipExtensionImports === true ? true : options.manageImports === false;
await loadEnvironmentExtensions(skip);
this._addSystems(this.config.systems);
this._addPipes(this.config.renderPipes, this.config.renderPipeAdaptors);
// loop through all systems...
for (const systemName in this._systemsHash)
{
const system = this._systemsHash[systemName];
const defaultSystemOptions = (system.constructor as any).defaultOptions;
options = { ...defaultSystemOptions, ...options };
}
options = { ...AbstractRenderer.defaultOptions, ...options };
this._roundPixels = options.roundPixels ? 1 : 0;
// await emits..
for (let i = 0; i < this.runners.init.items.length; i++)
{
await this.runners.init.items[i].init(options);
}
// store options
this._initOptions = options as OPTIONS;
}
/**
* Renders the object to its view.
* @param options - The options to render with.
* @param options.container - The container to render.
* @param [options.target] - The target to render to.
*/
public render(options: RenderOptions | Container): void;
/** @deprecated since 8.0.0 */
public render(container: Container, options: {renderTexture: any}): void;
public render(args: RenderOptions | Container, deprecated?: {renderTexture: any}): void
{
let options = args;
if (options instanceof Container)
{
options = { container: options };
if (deprecated)
{
// #if _DEBUG
// eslint-disable-next-line max-len
deprecation(v8_0_0, 'passing a second argument is deprecated, please use render options instead');
// #endif
options.target = deprecated.renderTexture;
}
}
options.target ||= this.view.renderTarget;
// TODO: we should eventually fix events so that it can handle multiple canvas elements
if (options.target === this.view.renderTarget)
{
// TODO get rid of this
this._lastObjectRendered = options.container;
options.clearColor = this.background.colorRgba;
}
if (options.clearColor)
{
const isRGBAArray = Array.isArray(options.clearColor) && options.clearColor.length === 4;
options.clearColor = isRGBAArray ? options.clearColor : Color.shared.setValue(options.clearColor).toArray();
}
if (!options.transform)
{
options.container.updateLocalTransform();
options.transform = options.container.localTransform;
}
this.runners.prerender.emit(options);
this.runners.renderStart.emit(options);
this.runners.render.emit(options);
this.runners.renderEnd.emit(options);
this.runners.postrender.emit(options);
}
/**
* Resizes the WebGL view to the specified width and height.
* @param desiredScreenWidth - The desired width of the screen.
* @param desiredScreenHeight - The desired height of the screen.
* @param resolution - The resolution / device pixel ratio of the renderer.
*/
public resize(desiredScreenWidth: number, desiredScreenHeight: number, resolution?: number): void
{
this.view.resize(desiredScreenWidth, desiredScreenHeight, resolution);
this.emit('resize', this.view.screen.width, this.view.screen.height);
}
public clear(options: ClearOptions = {}): void
{
// override!
const renderer = this as unknown as Renderer;
options.target ||= renderer.renderTarget.renderTarget;
options.clearColor ||= this.background.colorRgba;
options.clear ??= CLEAR.ALL;
const { clear, clearColor, target } = options;
Color.shared.setValue(clearColor ?? this.background.colorRgba);
renderer.renderTarget.clear(target, clear, Color.shared.toArray() as RgbaArray);
}
/** The resolution / device pixel ratio of the renderer. */
get resolution(): number
{
return this.view.resolution;
}
set resolution(value: number)
{
this.view.resolution = value;
this.runners.resolutionChange.emit(value);
}
/**
* Same as view.width, actual number of pixels in the canvas by horizontal.
* @member {number}
* @readonly
* @default 800
*/
get width(): number
{
return this.view.texture.frame.width;
}
/**
* Same as view.height, actual number of pixels in the canvas by vertical.
* @default 600
*/
get height(): number
{
return this.view.texture.frame.height;
}
// NOTE: this was `view` in v7
/**
* The canvas element that everything is drawn to.
* @type {environment.ICanvas}
*/
get canvas(): CANVAS
{
return this.view.canvas as CANVAS;
}
/**
* the last object rendered by the renderer. Useful for other plugins like interaction managers
* @readonly
*/
get lastObjectRendered(): Container
{
return this._lastObjectRendered;
}
/**
* Flag if we are rendering to the screen vs renderTexture
* @readonly
* @default true
*/
get renderingToScreen(): boolean
{
const renderer = this as unknown as Renderer;
return renderer.renderTarget.renderingToScreen;
}
/**
* Measurements of the screen. (0, 0, screenWidth, screenHeight).
*
* Its safe to use as filterArea or hitArea for the whole stage.
*/
get screen(): Rectangle
{
return this.view.screen;
}
/**
* Create a bunch of runners based of a collection of ids
* @param runnerIds - the runner ids to add
*/
private _addRunners(...runnerIds: string[]): void
{
runnerIds.forEach((runnerId) =>
{
this.runners[runnerId] = new SystemRunner(runnerId);
});
}
private _addSystems(systems: RendererConfig['systems']): void
{
let i: keyof typeof systems;
for (i in systems)
{
const val = systems[i];
this._addSystem(val.value, val.name);
}
}
/**
* Add a new system to the renderer.
* @param ClassRef - Class reference
* @param name - Property name for system, if not specified
* will use a static `name` property on the class itself. This
* name will be assigned as s property on the Renderer so make
* sure it doesn't collide with properties on Renderer.
* @returns Return instance of renderer
*/
private _addSystem(ClassRef: SystemConstructor, name: string): this
{
const system = new ClassRef(this as unknown as Renderer);
if ((this as any)[name])
{
throw new Error(`Whoops! The name "${name}" is already in use`);
}
(this as any)[name] = system;
this._systemsHash[name] = system;
for (const i in this.runners)
{
this.runners[i].add(system);
}
return this;
}
private _addPipes(pipes: RendererConfig['renderPipes'], pipeAdaptors: RendererConfig['renderPipeAdaptors']): void
{
const adaptors = pipeAdaptors.reduce((acc, adaptor) =>
{
acc[adaptor.name] = adaptor.value;
return acc;
}, {} as Record<string, any>);
pipes.forEach((pipe) =>
{
const PipeClass = pipe.value;
const name = pipe.name;
const Adaptor = adaptors[name];
// sorry typescript..
(this.renderPipes as any)[name] = new PipeClass(
this as unknown as Renderer,
Adaptor ? new Adaptor() : null
);
});
}
public destroy(options: RendererDestroyOptions = false): void
{
this.runners.destroy.items.reverse();
this.runners.destroy.emit(options);
// destroy all runners
Object.values(this.runners).forEach((runner) =>
{
runner.destroy();
});
this._systemsHash = null;
// destroy all pipes
(this.renderPipes as null) = null;
}
/**
* Generate a texture from a container.
* @param options - options or container target to use when generating the texture
* @returns a texture
*/
public generateTexture(options: GenerateTextureOptions | Container): Texture
{
return this.textureGenerator.generateTexture(options);
}
/**
* Whether the renderer will round coordinates to whole pixels when rendering.
* Can be overridden on a per scene item basis.
*/
get roundPixels(): boolean
{
return !!this._roundPixels;
}
/**
* Overrideable function by `pixi.js/unsafe-eval` to silence
* throwing an error if platform doesn't support unsafe-evals.
* @private
* @ignore
*/
public _unsafeEvalCheck(): void
{
if (!unsafeEvalSupported())
{
throw new Error('Current environment does not allow unsafe-eval, '
+ 'please use pixi.js/unsafe-eval module to enable support.');
}
}
}