diff --git a/examples/src/examples/graphics/outlines-colored.example.mjs b/examples/src/examples/graphics/outlines-colored.example.mjs index 7bfef730010..6bd3571e459 100644 --- a/examples/src/examples/graphics/outlines-colored.example.mjs +++ b/examples/src/examples/graphics/outlines-colored.example.mjs @@ -14,7 +14,6 @@ pc.WasmModule.setConfig('DracoDecoderModule', { const assets = { laboratory: new pc.Asset('statue', 'container', { url: `${rootPath}/static/assets/models/laboratory.glb` }), orbit: new pc.Asset('orbit', 'script', { url: `${rootPath}/static/scripts/camera/orbit-camera.js` }), - ssao: new pc.Asset('ssao', 'script', { url: `${rootPath}/static/scripts/posteffects/posteffect-ssao.js` }), helipad: new pc.Asset( 'helipad-env-atlas', 'texture', diff --git a/scripts/utils/cubemap-renderer.js b/scripts/utils/cubemap-renderer.js index 782c96ec9df..8db3f394d9b 100644 --- a/scripts/utils/cubemap-renderer.js +++ b/scripts/utils/cubemap-renderer.js @@ -67,6 +67,8 @@ CubemapRenderer.prototype.initialize = function () { ]; // set up rendering for all 6 faces + let firstCamera = null; + let lastCamera = null; for (var i = 0; i < 6; i++) { // render target, connected to cubemap texture face @@ -109,19 +111,29 @@ CubemapRenderer.prototype.initialize = function () { // set up its rotation e.setRotation(cameraRotations[i]); - // Before the first camera renders, trigger onCubemapPreRender event on the entity. - if (i === 0) { - e.camera.onPreRender = () => { - this.entity.fire('onCubemapPreRender'); - }; + // keep the first and last camera + if (i === 0) firstCamera = e.camera; + if (i === 5) lastCamera = e.camera; + } + + // Before the first camera renders, trigger onCubemapPreRender event on the entity. + this.evtPreRender = this.app.scene.on('prerender', (cameraComponent) => { + if (cameraComponent === firstCamera) { + this.entity.fire('onCubemapPreRender'); } + }); - // When last camera is finished rendering, trigger onCubemapPostRender event on the entity. - // This can be listened to by the user, and the resulting cubemap can be further processed (e.g prefiltered) - if (i === 5) { - e.camera.onPostRender = () => { - this.entity.fire('onCubemapPostRender'); - }; + // When last camera is finished rendering, trigger onCubemapPostRender event on the entity. + // This can be listened to by the user, and the resulting cubemap can be further processed (e.g pre-filtering) + this.evtPostRender = this.app.scene.on('postrender', (cameraComponent) => { + if (cameraComponent === lastCamera) { + this.entity.fire('onCubemapPostRender'); } - } + }); + + // when the script is destroyed, remove event listeners + this.on('destroy', () => { + this.evtPreRender.off(); + this.evtPostRender.off(); + }); }; diff --git a/scripts/utils/planar-renderer.js b/scripts/utils/planar-renderer.js index c979c923384..7c39df755c1 100644 --- a/scripts/utils/planar-renderer.js +++ b/scripts/utils/planar-renderer.js @@ -68,9 +68,16 @@ PlanarRenderer.prototype.initialize = function () { // When the camera is finished rendering, trigger onPlanarPostRender event on the entity. // This can be listened to by the user, and the resulting texture can be further processed (e.g prefiltered) - planarCamera.onPostRender = () => { - this.entity.fire('onPlanarPostRender'); - }; + this.evtPostRender = this.app.scene.on('postrender', (cameraComponent) => { + if (planarCamera === cameraComponent) { + this.entity.fire('onPlanarPostRender'); + } + }); + + // when the script is destroyed, remove event listeners + this.on('destroy', () => { + this.evtPostRender.off(); + }); }; PlanarRenderer.prototype.updateRenderTarget = function () { diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js index 1084144cf1f..e78a28c88f3 100644 --- a/src/deprecated/deprecated.js +++ b/src/deprecated/deprecated.js @@ -59,6 +59,7 @@ import { RigidBodyComponent } from '../framework/components/rigid-body/component import { RigidBodyComponentSystem } from '../framework/components/rigid-body/system.js'; import { LitShader } from '../scene/shader-lib/programs/lit-shader.js'; import { Geometry } from '../scene/geometry/geometry.js'; +import { CameraComponent } from '../framework/components/camera/component.js'; // MATH @@ -564,30 +565,37 @@ Object.defineProperty(Scene.prototype, 'models', { } }); -// A helper function to add deprecated set and get property on a Layer -function _removedLayerProperty(name) { - Object.defineProperty(Layer.prototype, name, { +// A helper function to add deprecated set and get property on a class +function _removedClassProperty(targetClass, name, comment = '') { + Object.defineProperty(targetClass.prototype, name, { set: function (value) { - Debug.errorOnce(`pc.Layer#${name} has been removed.`); + Debug.errorOnce(`${targetClass.name}#${name} has been removed. ${comment}`); }, get: function () { - Debug.errorOnce(`pc.Layer#${name} has been removed.`); + Debug.errorOnce(`${targetClass.name}#${name} has been removed. ${comment}`); return undefined; } }); } -_removedLayerProperty('renderTarget'); -_removedLayerProperty('onPreCull'); -_removedLayerProperty('onPreRender'); -_removedLayerProperty('onPreRenderOpaque'); -_removedLayerProperty('onPreRenderTransparent'); -_removedLayerProperty('onPostCull'); -_removedLayerProperty('onPostRender'); -_removedLayerProperty('onPostRenderOpaque'); -_removedLayerProperty('onPostRenderTransparent'); -_removedLayerProperty('onDrawCall'); -_removedLayerProperty('layerReference'); +_removedClassProperty(Layer, 'renderTarget'); +_removedClassProperty(Layer, 'onPreCull'); +_removedClassProperty(Layer, 'onPreRender'); +_removedClassProperty(Layer, 'onPreRenderOpaque'); +_removedClassProperty(Layer, 'onPreRenderTransparent'); +_removedClassProperty(Layer, 'onPostCull'); +_removedClassProperty(Layer, 'onPostRender'); +_removedClassProperty(Layer, 'onPostRenderOpaque'); +_removedClassProperty(Layer, 'onPostRenderTransparent'); +_removedClassProperty(Layer, 'onDrawCall'); +_removedClassProperty(Layer, 'layerReference'); + +_removedClassProperty(CameraComponent, 'onPreCull', 'Use Scene#EVENT_PRECULL event instead.'); +_removedClassProperty(CameraComponent, 'onPostCull', 'Use Scene#EVENT_POSTCULL event instead.'); +_removedClassProperty(CameraComponent, 'onPreRender', 'Use Scene#EVENT_PRERENDER event instead.'); +_removedClassProperty(CameraComponent, 'onPostRender', 'Use Scene#EVENT_POSTRENDER event instead.'); +_removedClassProperty(CameraComponent, 'onPreRenderLayer', 'Use Scene#EVENT_PRERENDER_LAYER event instead.'); +_removedClassProperty(CameraComponent, 'onPostRenderLayer', 'Use Scene#EVENT_POSTRENDER_LAYER event instead.'); ForwardRenderer.prototype.renderComposition = function (comp) { Debug.deprecated('pc.ForwardRenderer#renderComposition is deprecated. Use pc.AppBase.renderComposition instead.'); diff --git a/src/extras/renderers/outline-renderer.js b/src/extras/renderers/outline-renderer.js index f5fdacfdeec..3dbcdc85774 100644 --- a/src/extras/renderers/outline-renderer.js +++ b/src/extras/renderers/outline-renderer.js @@ -97,9 +97,11 @@ class OutlineRenderer { this.outlineShaderPass = this.outlineCameraEntity.camera.setShaderPass('OutlineShaderPass'); // function called after the camera has rendered the outline objects to the texture - this.outlineCameraEntity.camera.onPostRender = () => { - this.onPostRender(); - }; + app.scene.on('postrender', (cameraComponent) => { + if (this.outlineCameraEntity.camera === cameraComponent) { + this.onPostRender(); + } + }); // add the camera to the scene this.app.root.addChild(this.outlineCameraEntity); @@ -331,15 +333,12 @@ class OutlineRenderer { this.updateRenderTarget(sceneCamera); // function called before the scene camera renders a layer - sceneCameraEntity.camera.onPreRenderLayer = (layer, transparent) => { - - // when specified blend layer is rendered, add outline before its rendering - if (transparent === blendLayerTransparent && layer === blendLayer) { + const evt = this.app.scene.on('prerender:layer', (cameraComponent, layer, transparent) => { + if (sceneCamera === cameraComponent && transparent === blendLayerTransparent && layer === blendLayer) { this.blendOutlines(); - - sceneCameraEntity.camera.onPreRenderLayer = null; + evt.off(); } - }; + }); // copy the transform this.outlineCameraEntity.setLocalPosition(sceneCameraEntity.getPosition()); diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index c323dc244c3..8307433f010 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -29,14 +29,6 @@ import { PostEffectQueue } from './post-effect-queue.js'; * @param {number} view - Type of view. Can be {@link VIEW_CENTER}, {@link VIEW_LEFT} or {@link VIEW_RIGHT}. Left and right are only used in stereo rendering. */ -/** - * Callback used by {@link CameraComponent#onPreRenderLayer} and {@link CameraComponent#onPostRenderLayer}. - * - * @callback RenderLayerCallback - * @param {Layer} layer - The layer. - * @param {boolean} transparent - True for transparent sublayer, otherwise opaque sublayer. - */ - /** * The Camera Component enables an Entity to render the scene. A scene requires at least one * enabled camera component to be rendered. Note that multiple camera components can be enabled @@ -69,52 +61,6 @@ class CameraComponent extends Component { */ onPostprocessing = null; - /** - * Custom function that is called before the camera renders the scene. - * - * @type {Function|null} - */ - onPreRender = null; - - /** - * Custom function that is called before the camera renders a layer. This is called during - * rendering to a render target or a default framebuffer, and additional rendering can be - * performed here, for example using ${@link QuadRender#render}. - * - * @type {RenderLayerCallback|null} - */ - onPreRenderLayer = null; - - /** - * Custom function that is called after the camera renders the scene. - * - * @type {Function|null} - */ - onPostRender = null; - - /** - * Custom function that is called after the camera renders a layer. This is called during - * rendering to a render target or a default framebuffer, and additional rendering can be - * performed here, for example using ${@link QuadRender#render}. - * - * @type {RenderLayerCallback|null} - */ - onPostRenderLayer = null; - - /** - * Custom function that is called before visibility culling is performed for this camera. - * - * @type {Function|null} - */ - onPreCull = null; - - /** - * Custom function that is called after visibility culling is performed for this camera. - * - * @type {Function|null} - */ - onPostCull = null; - /** * A counter of requests of depth map rendering. * diff --git a/src/scene/constants.js b/src/scene/constants.js index 917e5fad06e..bf67f5a7cdd 100644 --- a/src/scene/constants.js +++ b/src/scene/constants.js @@ -1054,3 +1054,51 @@ export const DITHER_BLUENOISE = 'bluenoise'; * @category Graphics */ export const DITHER_IGNNOISE = 'ignnoise'; + +/** + * Name of event fired before the camera renders the scene. + * + * @type {string} + * @ignore + */ +export const EVENT_PRERENDER = 'prerender'; + +/** + * Name of event fired after the camera renders the scene. + * + * @type {string} + * @ignore + */ +export const EVENT_POSTRENDER = 'postrender'; + +/** + * Name of event fired before a layer is rendered by a camera. + * + * @type {string} + * @ignore + */ +export const EVENT_PRERENDER_LAYER = 'prerender:layer'; + +/** + * Name of event fired after a layer is rendered by a camera. + * + * @type {string} + * @ignore + */ +export const EVENT_POSTRENDER_LAYER = 'postrender:layer'; + +/** + * Name of event fired before visibility culling is performed for the camera + * + * @type {string} + * @ignore + */ +export const EVENT_PRECULL = 'precull'; + +/** + * Name of event after before visibility culling is performed for the camera + * + * @type {string} + * @ignore + */ +export const EVENT_POSTCULL = 'postcull'; diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js index d4827f4f7fd..4b8fe23f1eb 100644 --- a/src/scene/mesh-instance.js +++ b/src/scene/mesh-instance.js @@ -225,8 +225,8 @@ class MeshInstance { visible = true; /** - * Read this value in {@link CameraComponent#onPostCull} to determine if the object is actually going to - * be rendered. + * Read this value in {@link Scene#EVENT_POSTCULL} event to determine if the object is actually + * going to be rendered. * * @type {boolean} */ diff --git a/src/scene/renderer/render-pass-forward.js b/src/scene/renderer/render-pass-forward.js index 50d41d06b2a..407d5778ac7 100644 --- a/src/scene/renderer/render-pass-forward.js +++ b/src/scene/renderer/render-pass-forward.js @@ -6,7 +6,7 @@ import { BlendState } from '../../platform/graphics/blend-state.js'; import { DebugGraphics } from '../../platform/graphics/debug-graphics.js'; import { RenderPass } from '../../platform/graphics/render-pass.js'; import { RenderAction } from '../composition/render-action.js'; -import { SHADER_FORWARD } from '../constants.js'; +import { EVENT_POSTRENDER, EVENT_POSTRENDER_LAYER, EVENT_PRERENDER, EVENT_PRERENDER_LAYER, SHADER_FORWARD } from '../constants.js'; /** * @import { CameraComponent } from '../../framework/components/camera/component.js' @@ -201,11 +201,11 @@ class RenderPassForward extends RenderPass { before() { const { renderActions } = this; - // onPreRender callbacks + // onPreRender events for (let i = 0; i < renderActions.length; i++) { const ra = renderActions[i]; if (ra.firstCameraUse) { - ra.camera.onPreRender?.(); + this.scene.fire(EVENT_PRERENDER, ra.camera); } } } @@ -231,11 +231,11 @@ class RenderPassForward extends RenderPass { after() { - // onPostRender callbacks + // onPostRender events for (let i = 0; i < this.renderActions.length; i++) { const ra = this.renderActions[i]; if (ra.lastCameraUse) { - ra.camera.onPostRender?.(); + this.scene.fire(EVENT_POSTRENDER, ra.camera); } } @@ -249,7 +249,7 @@ class RenderPassForward extends RenderPass { */ renderRenderAction(renderAction, firstRenderAction) { - const { renderer } = this; + const { renderer, scene } = this; const device = renderer.device; // layer @@ -263,8 +263,8 @@ class RenderPassForward extends RenderPass { if (camera) { - // layer pre render callback - camera.onPreRenderLayer?.(layer, transparent); + // layer pre render event + scene.fire(EVENT_PRERENDER_LAYER, camera, layer, transparent); const options = { lightClusters: renderAction.lightClusters @@ -292,8 +292,8 @@ class RenderPassForward extends RenderPass { device.setStencilState(null, null); device.setAlphaToCoverage(false); - // layer post render callback - camera.onPostRenderLayer?.(layer, transparent); + // layer post render event + scene.fire(EVENT_POSTRENDER_LAYER, camera, layer, transparent); } DebugGraphics.popGpuMarker(this.device); diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js index d6debefa5fe..945f9a44234 100644 --- a/src/scene/renderer/renderer.js +++ b/src/scene/renderer/renderer.js @@ -25,7 +25,9 @@ import { SORTKEY_DEPTH, SORTKEY_FORWARD, VIEW_CENTER, PROJECTION_ORTHOGRAPHIC, LIGHTTYPE_DIRECTIONAL, MASK_AFFECT_DYNAMIC, MASK_AFFECT_LIGHTMAPPED, MASK_BAKE, - SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME + SHADOWUPDATE_NONE, SHADOWUPDATE_THISFRAME, + EVENT_PRECULL, + EVENT_POSTCULL } from '../constants.js'; import { LightCube } from '../graphics/light-cube.js'; import { getBlueNoiseTexture } from '../graphics/noise-textures.js'; @@ -1129,6 +1131,8 @@ class Renderer { const cullTime = now(); // #endif + const { scene } = this; + this.processingMeshInstances.clear(); // for all cameras @@ -1138,8 +1142,8 @@ class Renderer { for (let i = 0; i < numCameras; i++) { const camera = comp.cameras[i]; - // callback before the camera is culling - camera.onPreCull?.(); + // event before the camera is culling + scene?.fire(EVENT_PRECULL, camera); // update camera and frustum const renderTarget = camera.renderTarget; @@ -1162,13 +1166,13 @@ class Renderer { } } - // callback after the camera is done with culling - camera.onPostCull?.(); + // event after the camera is done with culling + scene?.fire(EVENT_POSTCULL, camera); } // update shadow / cookie atlas allocation for the visible lights. Update it after the ligthts were culled, // but before shadow maps were culling, as it might force some 'update once' shadows to cull. - if (this.scene.clusteredLightingEnabled) { + if (scene.clusteredLightingEnabled) { this.updateLightTextureAtlas(); } diff --git a/src/scene/scene.js b/src/scene/scene.js index a37a3153c5c..9554c2f8049 100644 --- a/src/scene/scene.js +++ b/src/scene/scene.js @@ -67,6 +67,80 @@ class Scene extends EventHandler { */ static EVENT_SETSKYBOX = 'set:skybox'; + /** + * Fired before the camera renders the scene. The handler is passed the {@link CameraComponent} + * that will render the scene. + * + * @event + * @example + * app.scene.on('prerender', (camera) => { + * console.log(`Camera ${camera.entity.name} will render the scene`); + * }); + */ + static EVENT_PRERENDER = 'prerender'; + + /** + * Fired when the camera renders the scene. The handler is passed the {@link CameraComponent} + * that rendered the scene. + * + * @event + * @example + * app.scene.on('postrender', (camera) => { + * console.log(`Camera ${camera.entity.name} rendered the scene`); + * }); + */ + static EVENT_POSTRENDER = 'postrender'; + + /** + * Fired before the camera renders a layer. The handler is passed the {@link CameraComponent}, + * the {@link Layer} that will be rendered, and a boolean parameter set to true if the layer is + * transparent. This is called during rendering to a render target or a default framebuffer, and + * additional rendering can be performed here, for example using {@link QuadRender#render}. + * + * @event + * @example + * app.scene.on('prerender:layer', (camera, layer, transparent) => { + * console.log(`Camera ${camera.entity.name} will render the layer ${layer.name} (transparent: ${transparent})`); + * }); + */ + static EVENT_PRERENDER_LAYER = 'prerender:layer'; + + /** + * Fired when the camera renders a layer. The handler is passed the {@link CameraComponent}, + * the {@link Layer} that will be rendered, and a boolean parameter set to true if the layer is + * transparent. This is called during rendering to a render target or a default framebuffer, and + * additional rendering can be performed here, for example using {@link QuadRender#render}. + * + * @event + * @example + * app.scene.on('postrender:layer', (camera, layer, transparent) => { + * console.log(`Camera ${camera.entity.name} rendered the layer ${layer.name} (transparent: ${transparent})`); + * }); + */ + static EVENT_POSTRENDER_LAYER = 'postrender:layer'; + + /** + * Fired before visibility culling is performed for the camera. + * + * @event + * @example + * app.scene.on('precull', (camera) => { + * console.log(`Visibility culling will be performed for camera ${camera.entity.name}`); + * }); + */ + static EVENT_PRECULL = 'precull'; + + /** + * Fired after visibility culling is performed for the camera. + * + * @event + * @example + * app.scene.on('postcull', (camera) => { + * console.log(`Visibility culling was performed for camera ${camera.entity.name}`); + * }); + */ + static EVENT_POSTCULL = 'postcull'; + /** * If enabled, the ambient lighting will be baked into lightmaps. This will be either the * {@link Scene#skybox} if set up, otherwise {@link Scene#ambientLight}. Defaults to false.