playcanvas
Version:
PlayCanvas WebGL game engine
1,105 lines (1,102 loc) • 41.3 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { LAYERID_UI, LAYERID_DEPTH, ASPECT_AUTO } from '../../../scene/constants.js';
import { Camera } from '../../../scene/camera.js';
import { ShaderPass } from '../../../scene/shader-pass.js';
import { Component } from '../component.js';
import { PostEffectQueue } from './post-effect-queue.js';
/**
* @import { CameraComponentSystem } from './system.js'
* @import { Color } from '../../../core/math/color.js'
* @import { Entity } from '../../entity.js'
* @import { EventHandle } from '../../../core/event-handle.js'
* @import { Frustum } from '../../../core/shape/frustum.js'
* @import { LayerComposition } from '../../../scene/composition/layer-composition.js'
* @import { Layer } from '../../../scene/layer.js'
* @import { Mat4 } from '../../../core/math/mat4.js'
* @import { RenderPass } from '../../../platform/graphics/render-pass.js'
* @import { RenderTarget } from '../../../platform/graphics/render-target.js'
* @import { FogParams } from '../../../scene/fog-params.js'
* @import { Vec3 } from '../../../core/math/vec3.js'
* @import { Vec4 } from '../../../core/math/vec4.js'
* @import { XrErrorCallback } from '../../xr/xr-manager.js'
*/ /**
* @callback CalculateMatrixCallback
* Callback used by {@link CameraComponent#calculateTransform} and {@link CameraComponent#calculateProjection}.
* @param {Mat4} transformMatrix - Output of the function.
* @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.
* @returns {void}
*/ /**
* The CameraComponent enables an {@link Entity} to render the scene. A scene requires at least
* one enabled camera component to be rendered. The camera's view direction is along the negative
* z-axis of the owner entity.
*
* Note that multiple camera components can be enabled simultaneously (for split-screen or
* offscreen rendering, for example).
*
* You should never need to use the CameraComponent constructor directly. To add a CameraComponent
* to an {@link Entity}, use {@link Entity#addComponent}:
*
* ```javascript
* const entity = new pc.Entity();
* entity.addComponent('camera', {
* nearClip: 1,
* farClip: 100,
* fov: 55
* });
* ```
*
* Once the CameraComponent is added to the entity, you can access it via the {@link Entity#camera}
* property:
*
* ```javascript
* entity.camera.nearClip = 2; // Set the near clip of the camera
*
* console.log(entity.camera.nearClip); // Get the near clip of the camera
* ```
*
* @hideconstructor
* @category Graphics
*/ class CameraComponent extends Component {
/**
* Create a new CameraComponent instance.
*
* @param {CameraComponentSystem} system - The ComponentSystem that created this Component.
* @param {Entity} entity - The Entity that this Component is attached to.
*/ constructor(system, entity){
super(system, entity), /**
* Custom function that is called when postprocessing should execute.
*
* @type {Function|null}
* @ignore
*/ this.onPostprocessing = null, /**
* A counter of requests of depth map rendering.
*
* @type {number}
* @private
*/ this._renderSceneDepthMap = 0, /**
* A counter of requests of color map rendering.
*
* @type {number}
* @private
*/ this._renderSceneColorMap = 0, /** @private */ this._sceneDepthMapRequested = false, /** @private */ this._sceneColorMapRequested = false, /** @private */ this._priority = 0, /**
* Layer id at which the postprocessing stops for the camera.
*
* @type {number}
* @private
*/ this._disablePostEffectsLayer = LAYERID_UI, /** @private */ this._camera = new Camera(), /**
* @type {EventHandle|null}
* @private
*/ this._evtLayersChanged = null, /**
* @type {EventHandle|null}
* @private
*/ this._evtLayerAdded = null, /**
* @type {EventHandle|null}
* @private
*/ this._evtLayerRemoved = null;
this._camera.node = entity;
// postprocessing management
this._postEffects = new PostEffectQueue(system.app, this);
}
/**
* Sets the name of the shader pass the camera will use when rendering.
*
* In addition to existing names (see the parameter description), a new name can be specified,
* which creates a new shader pass with the given name. The name provided can only use
* alphanumeric characters and underscores. When a shader is compiled for the new pass, a define
* is added to the shader. For example, if the name is 'custom_rendering', the define
* 'CUSTOM_RENDERING_PASS' is added to the shader, allowing the shader code to conditionally
* execute code only when that shader pass is active.
*
* Another instance where this approach may prove useful is when a camera needs to render a more
* cost-effective version of shaders, such as when creating a reflection texture. To accomplish
* this, a callback on the material that triggers during shader compilation can be used. This
* callback can modify the shader generation options specifically for this shader pass.
*
* ```javascript
* const shaderPassId = camera.setShaderPass('custom_rendering');
*
* material.onUpdateShader = function (options) {
* if (options.pass === shaderPassId) {
* options.litOptions.normalMapEnabled = false;
* options.litOptions.useSpecular = false;
* }
* return options;
* };
* ```
*
* @param {string} name - The name of the shader pass. Defaults to undefined, which is
* equivalent to {@link SHADERPASS_FORWARD}. Can be:
*
* - {@link SHADERPASS_FORWARD}
* - {@link SHADERPASS_ALBEDO}
* - {@link SHADERPASS_OPACITY}
* - {@link SHADERPASS_WORLDNORMAL}
* - {@link SHADERPASS_SPECULARITY}
* - {@link SHADERPASS_GLOSS}
* - {@link SHADERPASS_METALNESS}
* - {@link SHADERPASS_AO}
* - {@link SHADERPASS_EMISSION}
* - {@link SHADERPASS_LIGHTING}
* - {@link SHADERPASS_UV0}
*
* @returns {number} The id of the shader pass.
*/ setShaderPass(name) {
const shaderPass = ShaderPass.get(this.system.app.graphicsDevice);
const shaderPassInfo = name ? shaderPass.allocate(name, {
isForward: true
}) : null;
this._camera.shaderPassInfo = shaderPassInfo;
return shaderPassInfo.index;
}
/**
* Shader pass name.
*
* @returns {string|undefined} The name of the shader pass, or undefined if no shader pass is set.
*/ getShaderPass() {
return this._camera.shaderPassInfo?.name;
}
/**
* Sets the render passes the camera uses for rendering, instead of its default rendering.
* Set this to null to return to the default behavior.
*
* @type {RenderPass[]|null}
* @ignore
*/ set renderPasses(passes) {
this._camera.renderPasses = passes || [];
this.dirtyLayerCompositionCameras();
this.system.app.scene.updateShaders = true;
}
/**
* Gets the render passes the camera uses for rendering, instead of its default rendering.
*
* @type {RenderPass[]}
* @ignore
*/ get renderPasses() {
return this._camera.renderPasses;
}
get shaderParams() {
return this._camera.shaderParams;
}
/**
* Sets the gamma correction to apply when rendering the scene. Can be:
*
* - {@link GAMMA_NONE}
* - {@link GAMMA_SRGB}
*
* Defaults to {@link GAMMA_SRGB}.
*
* @type {number}
*/ set gammaCorrection(value) {
this.camera.shaderParams.gammaCorrection = value;
}
/**
* Gets the gamma correction used when rendering the scene.
*
* @type {number}
*/ get gammaCorrection() {
return this.camera.shaderParams.gammaCorrection;
}
/**
* Sets the tonemapping transform to apply to the rendered color buffer. Can be:
*
* - {@link TONEMAP_LINEAR}
* - {@link TONEMAP_FILMIC}
* - {@link TONEMAP_HEJL}
* - {@link TONEMAP_ACES}
* - {@link TONEMAP_ACES2}
* - {@link TONEMAP_NEUTRAL}
*
* Defaults to {@link TONEMAP_LINEAR}.
*
* @type {number}
*/ set toneMapping(value) {
this.camera.shaderParams.toneMapping = value;
}
/**
* Gets the tonemapping transform applied to the rendered color buffer.
*
* @type {number}
*/ get toneMapping() {
return this.camera.shaderParams.toneMapping;
}
/**
* Sets the fog parameters. If this is not null, the camera will use these fog parameters
* instead of those specified on the {@link Scene#fog}.
*
* @type {FogParams|null}
*/ set fog(value) {
this._camera.fogParams = value;
}
/**
* Gets a {@link FogParams} that defines fog parameters, or null if those are not set.
*
* @type {FogParams|null}
*/ get fog() {
return this._camera.fogParams;
}
/**
* Sets the camera aperture in f-stops. Default is 16. Higher value means less exposure. Used
* if {@link Scene#physicalUnits} is true.
*
* @type {number}
*/ set aperture(value) {
this._camera.aperture = value;
}
/**
* Gets the camera aperture in f-stops.
*
* @type {number}
*/ get aperture() {
return this._camera.aperture;
}
/**
* Sets the aspect ratio (width divided by height) of the camera. If {@link aspectRatioMode} is
* {@link ASPECT_AUTO}, then this value will be automatically calculated every frame, and you
* can only read it. If it's {@link ASPECT_MANUAL}, you can set the value.
*
* @type {number}
*/ set aspectRatio(value) {
this._camera.aspectRatio = value;
}
/**
* Gets the aspect ratio (width divided by height) of the camera.
*
* @type {number}
*/ get aspectRatio() {
return this._camera.aspectRatio;
}
/**
* Sets the aspect ratio mode of the camera. Can be:
*
* - {@link ASPECT_AUTO}: aspect ratio will be calculated from the current render
* target's width divided by height.
* - {@link ASPECT_MANUAL}: use the aspectRatio value.
*
* Defaults to {@link ASPECT_AUTO}.
*
* @type {number}
*/ set aspectRatioMode(value) {
this._camera.aspectRatioMode = value;
}
/**
* Gets the aspect ratio mode of the camera.
*
* @type {number}
*/ get aspectRatioMode() {
return this._camera.aspectRatioMode;
}
/**
* Sets the custom function to calculate the camera projection matrix manually. Can be used for
* complex effects like doing oblique projection. Function is called using component's scope.
*
* Arguments:
*
* - {@link Mat4} transformMatrix: output of the function
* - 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.
*
* @type {CalculateMatrixCallback}
*/ set calculateProjection(value) {
this._camera.calculateProjection = value;
}
/**
* Gets the custom function to calculate the camera projection matrix manually.
*
* @type {CalculateMatrixCallback}
*/ get calculateProjection() {
return this._camera.calculateProjection;
}
/**
* Sets the custom function to calculate the camera transformation matrix manually. Can be used
* for complex effects like reflections. Function is called using component's scope. Arguments:
*
* - {@link Mat4} transformMatrix: output of the function.
* - 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.
*
* @type {CalculateMatrixCallback}
*/ set calculateTransform(value) {
this._camera.calculateTransform = value;
}
/**
* Gets the custom function to calculate the camera transformation matrix manually.
*
* @type {CalculateMatrixCallback}
*/ get calculateTransform() {
return this._camera.calculateTransform;
}
/**
* Gets the camera component's underlying Camera instance.
*
* @type {Camera}
* @ignore
*/ get camera() {
return this._camera;
}
/**
* Sets the camera component's clear color. Defaults to `[0.75, 0.75, 0.75, 1]`.
*
* @type {Color}
*/ set clearColor(value) {
this._camera.clearColor = value;
}
/**
* Gets the camera component's clear color.
*
* @type {Color}
*/ get clearColor() {
return this._camera.clearColor;
}
/**
* Sets whether the camera will automatically clear the color buffer before rendering. Defaults to true.
*
* @type {boolean}
*/ set clearColorBuffer(value) {
this._camera.clearColorBuffer = value;
this.dirtyLayerCompositionCameras();
}
/**
* Gets whether the camera will automatically clear the color buffer before rendering.
*
* @type {boolean}
*/ get clearColorBuffer() {
return this._camera.clearColorBuffer;
}
/**
* Sets the depth value to clear the depth buffer to. Defaults to 1.
*
* @type {number}
*/ set clearDepth(value) {
this._camera.clearDepth = value;
}
/**
* Gets the depth value to clear the depth buffer to.
*
* @type {number}
*/ get clearDepth() {
return this._camera.clearDepth;
}
/**
* Sets whether the camera will automatically clear the depth buffer before rendering. Defaults to true.
*
* @type {boolean}
*/ set clearDepthBuffer(value) {
this._camera.clearDepthBuffer = value;
this.dirtyLayerCompositionCameras();
}
/**
* Gets whether the camera will automatically clear the depth buffer before rendering.
*
* @type {boolean}
*/ get clearDepthBuffer() {
return this._camera.clearDepthBuffer;
}
/**
* Sets whether the camera will automatically clear the stencil buffer before rendering. Defaults to true.
*
* @type {boolean}
*/ set clearStencilBuffer(value) {
this._camera.clearStencilBuffer = value;
this.dirtyLayerCompositionCameras();
}
/**
* Gets whether the camera will automatically clear the stencil buffer before rendering.
*
* @type {boolean}
*/ get clearStencilBuffer() {
return this._camera.clearStencilBuffer;
}
/**
* Sets whether the camera will cull triangle faces. If true, the camera will take
* {@link Material#cull} into account. Otherwise both front and back faces will be rendered.
* Defaults to true.
*
* @type {boolean}
*/ set cullFaces(value) {
this._camera.cullFaces = value;
}
/**
* Gets whether the camera will cull triangle faces.
*
* @type {boolean}
*/ get cullFaces() {
return this._camera.cullFaces;
}
/**
* Sets the layer id of the layer on which the post-processing of the camera stops being applied
* to. Defaults to {@link LAYERID_UI}, which causes post-processing to not be applied to UI
* layer and any following layers for the camera. Set to `undefined` for post-processing to be
* applied to all layers of the camera.
*
* @type {number}
*/ set disablePostEffectsLayer(layer) {
this._disablePostEffectsLayer = layer;
this.dirtyLayerCompositionCameras();
}
/**
* Gets the layer id of the layer on which the post-processing of the camera stops being applied
* to.
*
* @type {number}
*/ get disablePostEffectsLayer() {
return this._disablePostEffectsLayer;
}
/**
* Sets the distance from the camera after which no rendering will take place. Defaults to 1000.
*
* @type {number}
*/ set farClip(value) {
this._camera.farClip = value;
}
/**
* Gets the distance from the camera after which no rendering will take place.
*
* @type {number}
*/ get farClip() {
return this._camera.farClip;
}
/**
* Sets whether the camera will flip the face direction of triangles. If set to true, the
* camera will invert front and back faces. Can be useful for reflection rendering. Defaults to
* false.
*
* @type {boolean}
*/ set flipFaces(value) {
this._camera.flipFaces = value;
}
/**
* Gets whether the camera will flip the face direction of triangles.
*
* @type {boolean}
*/ get flipFaces() {
return this._camera.flipFaces;
}
/**
* Sets the field of view of the camera in degrees. Usually this is the Y-axis field of view
* (see {@link horizontalFov}). Used for {@link PROJECTION_PERSPECTIVE} cameras only. Defaults to
* 45.
*
* @type {number}
*/ set fov(value) {
this._camera.fov = value;
}
/**
* Gets the field of view of the camera in degrees.
*
* @type {number}
*/ get fov() {
return this._camera.fov;
}
/**
* Gets the camera's frustum shape.
*
* @type {Frustum}
*/ get frustum() {
return this._camera.frustum;
}
/**
* Sets whether frustum culling is enabled. This controls the culling of {@link MeshInstance}s
* against the camera frustum, i.e. if objects outside of the camera's frustum should be
* omitted from rendering. If false, all mesh instances in the scene are rendered by the
* camera, regardless of visibility. Defaults to false.
*
* @type {boolean}
*/ set frustumCulling(value) {
this._camera.frustumCulling = value;
}
/**
* Gets whether frustum culling is enabled.
*
* @type {boolean}
*/ get frustumCulling() {
return this._camera.frustumCulling;
}
/**
* Sets whether the camera's field of view ({@link fov}) is horizontal or vertical. Defaults to
* false (meaning it is vertical by default).
*
* @type {boolean}
*/ set horizontalFov(value) {
this._camera.horizontalFov = value;
}
/**
* Gets whether the camera's field of view ({@link fov}) is horizontal or vertical.
*
* @type {boolean}
*/ get horizontalFov() {
return this._camera.horizontalFov;
}
/**
* Sets the array of layer IDs ({@link Layer#id}) to which this camera should belong. Don't
* push, pop, splice or modify this array. If you want to change it, set a new one instead.
* Defaults to [{@link LAYERID_WORLD}, {@link LAYERID_DEPTH}, {@link LAYERID_SKYBOX},
* {@link LAYERID_UI}, {@link LAYERID_IMMEDIATE}].
*
* @type {number[]}
*/ set layers(newValue) {
const oldLayers = this._camera.layers;
const scene = this.system.app.scene;
// Remove from old layers
oldLayers.forEach((layerId)=>{
const layer = scene.layers.getLayerById(layerId);
layer?.removeCamera(this);
});
this._camera.layers = newValue;
// Only add to new layers if enabled
if (this.enabled && this.entity.enabled) {
newValue.forEach((layerId)=>{
const layer = scene.layers.getLayerById(layerId);
layer?.addCamera(this);
});
}
this.fire('set:layers');
}
/**
* Gets the array of layer IDs ({@link Layer#id}) to which this camera belongs.
*
* @type {number[]}
*/ get layers() {
return this._camera.layers;
}
get layersSet() {
return this._camera.layersSet;
}
/**
* Sets the jitter intensity applied in the projection matrix. Used for jittered sampling by TAA.
* A value of 1 represents a jitter in the range of `[-1, 1]` of a pixel. Smaller values result
* in a crisper yet more aliased outcome, whereas increased values produce a smoother but blurred
* result. Defaults to 0, representing no jitter.
*
* @type {number}
*/ set jitter(value) {
this._camera.jitter = value;
}
/**
* Gets the jitter intensity applied in the projection matrix.
*
* @type {number}
*/ get jitter() {
return this._camera.jitter;
}
/**
* Sets the distance from the camera before which no rendering will take place. Defaults to 0.1.
*
* @type {number}
*/ set nearClip(value) {
this._camera.nearClip = value;
}
/**
* Gets the distance from the camera before which no rendering will take place.
*
* @type {number}
*/ get nearClip() {
return this._camera.nearClip;
}
/**
* Sets the half-height of the orthographic view window (in the Y-axis). Used for
* {@link PROJECTION_ORTHOGRAPHIC} cameras only. Defaults to 10.
*
* @type {number}
*/ set orthoHeight(value) {
this._camera.orthoHeight = value;
}
/**
* Gets the half-height of the orthographic view window (in the Y-axis).
*
* @type {number}
*/ get orthoHeight() {
return this._camera.orthoHeight;
}
/**
* Gets the post effects queue for this camera. Use this to add or remove post effects from the
* camera.
*
* @type {PostEffectQueue}
*/ get postEffects() {
return this._postEffects;
}
get postEffectsEnabled() {
return this._postEffects.enabled;
}
/**
* Sets the priority to control the render order of this camera. Cameras with a smaller
* priority value are rendered first. Defaults to 0.
*
* @type {number}
*/ set priority(newValue) {
this._priority = newValue;
this.dirtyLayerCompositionCameras();
}
/**
* Gets the priority to control the render order of this camera.
*
* @type {number}
*/ get priority() {
return this._priority;
}
/**
* Sets the type of projection used to render the camera. Can be:
*
* - {@link PROJECTION_PERSPECTIVE}: A perspective projection. The camera frustum
* resembles a truncated pyramid.
* - {@link PROJECTION_ORTHOGRAPHIC}: An orthographic projection. The camera
* frustum is a cuboid.
*
* Defaults to {@link PROJECTION_PERSPECTIVE}.
*
* @type {number}
*/ set projection(value) {
this._camera.projection = value;
}
/**
* Gets the type of projection used to render the camera.
*
* @type {number}
*/ get projection() {
return this._camera.projection;
}
/**
* Gets the camera's projection matrix.
*
* @type {Mat4}
*/ get projectionMatrix() {
return this._camera.projectionMatrix;
}
/**
* Sets the rendering rectangle for the camera. This controls where on the screen the camera
* will render in normalized screen coordinates. Defaults to `[0, 0, 1, 1]`.
*
* @type {Vec4}
*/ set rect(value) {
this._camera.rect = value;
this.fire('set:rect', this._camera.rect);
}
/**
* Gets the rendering rectangle for the camera.
*
* @type {Vec4}
*/ get rect() {
return this._camera.rect;
}
set renderSceneColorMap(value) {
if (value && !this._sceneColorMapRequested) {
this.requestSceneColorMap(true);
this._sceneColorMapRequested = true;
} else if (this._sceneColorMapRequested) {
this.requestSceneColorMap(false);
this._sceneColorMapRequested = false;
}
}
get renderSceneColorMap() {
return this._renderSceneColorMap > 0;
}
set renderSceneDepthMap(value) {
if (value && !this._sceneDepthMapRequested) {
this.requestSceneDepthMap(true);
this._sceneDepthMapRequested = true;
} else if (this._sceneDepthMapRequested) {
this.requestSceneDepthMap(false);
this._sceneDepthMapRequested = false;
}
}
get renderSceneDepthMap() {
return this._renderSceneDepthMap > 0;
}
/**
* Sets the render target to which rendering of the camera is performed. If not set, it will
* render simply to the screen.
*
* @type {RenderTarget}
*/ set renderTarget(value) {
Debug.call(()=>{
if (this._camera.renderPasses.length > 0) {
Debug.warn(`Setting a render target on the camera ${this.entity.name} after the render passes is not supported, set it up first.`);
}
});
this._camera.renderTarget = value;
this.dirtyLayerCompositionCameras();
}
/**
* Gets the render target to which rendering of the camera is performed.
*
* @type {RenderTarget}
*/ get renderTarget() {
return this._camera.renderTarget;
}
/**
* Sets the scissor rectangle for the camera. This clips all pixels which are not in the
* rectangle. The order of the values is `[x, y, width, height]`. Defaults to `[0, 0, 1, 1]`.
*
* @type {Vec4}
*/ set scissorRect(value) {
this._camera.scissorRect = value;
}
/**
* Gets the scissor rectangle for the camera.
*
* @type {Vec4}
*/ get scissorRect() {
return this._camera.scissorRect;
}
/**
* Sets the camera sensitivity in ISO. Defaults to 1000. Higher value means more exposure. Used
* if {@link Scene#physicalUnits} is true.
*
* @type {number}
*/ set sensitivity(value) {
this._camera.sensitivity = value;
}
/**
* Gets the camera sensitivity in ISO.
*
* @type {number}
*/ get sensitivity() {
return this._camera.sensitivity;
}
/**
* Sets the camera shutter speed in seconds. Defaults to 1/1000s. Longer shutter means more
* exposure. Used if {@link Scene#physicalUnits} is true.
*
* @type {number}
*/ set shutter(value) {
this._camera.shutter = value;
}
/**
* Gets the camera shutter speed in seconds.
*
* @type {number}
*/ get shutter() {
return this._camera.shutter;
}
/**
* Gets the camera's view matrix.
*
* @type {Mat4}
*/ get viewMatrix() {
return this._camera.viewMatrix;
}
/**
* Based on the value, the depth layer's enable counter is incremented or decremented.
*
* @param {boolean} value - True to increment the counter, false to decrement it.
* @returns {boolean} True if the counter was incremented or decremented, false if the depth
* layer is not present.
* @private
*/ _enableDepthLayer(value) {
const hasDepthLayer = this.layers.find((layerId)=>layerId === LAYERID_DEPTH);
if (hasDepthLayer) {
/** @type {Layer} */ const depthLayer = this.system.app.scene.layers.getLayerById(LAYERID_DEPTH);
if (value) {
depthLayer?.incrementCounter();
} else {
depthLayer?.decrementCounter();
}
} else if (value) {
return false;
}
return true;
}
/**
* Request the scene to generate a texture containing the scene color map. Note that this call
* is accumulative, and for each enable request, a disable request need to be called. Note that
* this setting is ignored when the {@link CameraComponent#renderPasses} is used.
*
* @param {boolean} enabled - True to request the generation, false to disable it.
*/ requestSceneColorMap(enabled) {
this._renderSceneColorMap += enabled ? 1 : -1;
Debug.assert(this._renderSceneColorMap >= 0);
const ok = this._enableDepthLayer(enabled);
if (!ok) {
Debug.warnOnce('CameraComponent.requestSceneColorMap was called, but the camera does not have a Depth layer, ignoring.');
}
this.camera._enableRenderPassColorGrab(this.system.app.graphicsDevice, this.renderSceneColorMap);
this.system.app.scene.layers.markDirty();
}
/**
* Request the scene to generate a texture containing the scene depth map. Note that this call
* is accumulative, and for each enable request, a disable request need to be called. Note that
* this setting is ignored when the {@link CameraComponent#renderPasses} is used.
*
* @param {boolean} enabled - True to request the generation, false to disable it.
*/ requestSceneDepthMap(enabled) {
this._renderSceneDepthMap += enabled ? 1 : -1;
Debug.assert(this._renderSceneDepthMap >= 0);
const ok = this._enableDepthLayer(enabled);
if (!ok) {
Debug.warnOnce('CameraComponent.requestSceneDepthMap was called, but the camera does not have a Depth layer, ignoring.');
}
this.camera._enableRenderPassDepthGrab(this.system.app.graphicsDevice, this.system.app.renderer, this.renderSceneDepthMap);
this.system.app.scene.layers.markDirty();
}
dirtyLayerCompositionCameras() {
// layer composition needs to update order
const layerComp = this.system.app.scene.layers;
layerComp._dirty = true;
}
/**
* Convert a point from 2D screen space to 3D world space.
*
* @param {number} screenx - X coordinate on PlayCanvas' canvas element. Should be in the range
* 0 to `canvas.offsetWidth` of the application's canvas element.
* @param {number} screeny - Y coordinate on PlayCanvas' canvas element. Should be in the range
* 0 to `canvas.offsetHeight` of the application's canvas element.
* @param {number} cameraz - The distance from the camera in world space to create the new
* point.
* @param {Vec3} [worldCoord] - 3D vector to receive world coordinate result.
* @example
* // Get the start and end points of a 3D ray fired from a screen click position
* const start = entity.camera.screenToWorld(clickX, clickY, entity.camera.nearClip);
* const end = entity.camera.screenToWorld(clickX, clickY, entity.camera.farClip);
*
* // Use the ray coordinates to perform a raycast
* app.systems.rigidbody.raycastFirst(start, end, function (result) {
* console.log("Entity " + result.entity.name + " was selected");
* });
* @returns {Vec3} The world space coordinate.
*/ screenToWorld(screenx, screeny, cameraz, worldCoord) {
const device = this.system.app.graphicsDevice;
const { width, height } = device.clientRect;
return this._camera.screenToWorld(screenx, screeny, cameraz, width, height, worldCoord);
}
/**
* Convert a point from 3D world space to 2D screen space.
*
* @param {Vec3} worldCoord - The world space coordinate.
* @param {Vec3} [screenCoord] - 3D vector to receive screen coordinate result.
* @returns {Vec3} The screen space coordinate.
*/ worldToScreen(worldCoord, screenCoord) {
const device = this.system.app.graphicsDevice;
const { width, height } = device.clientRect;
return this._camera.worldToScreen(worldCoord, width, height, screenCoord);
}
/**
* Called before application renders the scene.
*
* @ignore
*/ onAppPrerender() {
this._camera._viewMatDirty = true;
this._camera._viewProjMatDirty = true;
}
/** @private */ addCameraToLayers() {
const layers = this.layers;
for(let i = 0; i < layers.length; i++){
const layer = this.system.app.scene.layers.getLayerById(layers[i]);
if (layer) {
layer.addCamera(this);
}
}
}
/** @private */ removeCameraFromLayers() {
const layers = this.layers;
for(let i = 0; i < layers.length; i++){
const layer = this.system.app.scene.layers.getLayerById(layers[i]);
if (layer) {
layer.removeCamera(this);
}
}
}
/**
* @param {LayerComposition} oldComp - Old layer composition.
* @param {LayerComposition} newComp - New layer composition.
* @private
*/ onLayersChanged(oldComp, newComp) {
this.addCameraToLayers();
oldComp.off('add', this.onLayerAdded, this);
oldComp.off('remove', this.onLayerRemoved, this);
newComp.on('add', this.onLayerAdded, this);
newComp.on('remove', this.onLayerRemoved, this);
}
/**
* @param {Layer} layer - The layer to add the camera to.
* @private
*/ onLayerAdded(layer) {
const index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.addCamera(this);
}
/**
* @param {Layer} layer - The layer to remove the camera from.
* @private
*/ onLayerRemoved(layer) {
const index = this.layers.indexOf(layer.id);
if (index < 0) return;
layer.removeCamera(this);
}
onEnable() {
const scene = this.system.app.scene;
const layers = scene.layers;
this.system.addCamera(this);
this._evtLayersChanged?.off();
this._evtLayersChanged = scene.on('set:layers', this.onLayersChanged, this);
if (layers) {
this._evtLayerAdded?.off();
this._evtLayerAdded = layers.on('add', this.onLayerAdded, this);
this._evtLayerRemoved?.off();
this._evtLayerRemoved = layers.on('remove', this.onLayerRemoved, this);
}
if (this.enabled && this.entity.enabled) {
this.addCameraToLayers();
}
this.postEffects.enable();
}
onDisable() {
const scene = this.system.app.scene;
const layers = scene.layers;
this.postEffects.disable();
this.removeCameraFromLayers();
this._evtLayersChanged?.off();
this._evtLayersChanged = null;
if (layers) {
this._evtLayerAdded?.off();
this._evtLayerAdded = null;
this._evtLayerRemoved?.off();
this._evtLayerRemoved = null;
}
this.system.removeCamera(this);
}
onRemove() {
this.onDisable();
this.off();
this.camera.destroy();
}
/**
* Calculates aspect ratio value for a given render target.
*
* @param {RenderTarget|null} [rt] - Optional
* render target. If unspecified, the backbuffer is used.
* @returns {number} The aspect ratio of the render target (or backbuffer).
*/ calculateAspectRatio(rt) {
const device = this.system.app.graphicsDevice;
const width = rt ? rt.width : device.width;
const height = rt ? rt.height : device.height;
return width * this.rect.z / (height * this.rect.w);
}
/**
* Prepare the camera for frame rendering.
*
* @param {RenderTarget|null} [rt] - Render
* target to which rendering will be performed. Will affect camera's aspect ratio, if
* aspectRatioMode is {@link ASPECT_AUTO}.
* @ignore
*/ frameUpdate(rt) {
if (this.aspectRatioMode === ASPECT_AUTO) {
this.aspectRatio = this.calculateAspectRatio(rt);
}
}
/**
* Attempt to start XR session with this camera.
*
* @param {string} type - The type of session. Can be one of the following:
*
* - {@link XRTYPE_INLINE}: Inline - always available type of session. It has limited feature
* availability and is rendered into HTML element.
* - {@link XRTYPE_VR}: Immersive VR - session that provides exclusive access to the VR device
* with the best available tracking features.
* - {@link XRTYPE_AR}: Immersive AR - session that provides exclusive access to the VR/AR
* device that is intended to be blended with the real-world environment.
*
* @param {string} spaceType - Reference space type. Can be one of the following:
*
* - {@link XRSPACE_VIEWER}: Viewer - always supported space with some basic tracking
* capabilities.
* - {@link XRSPACE_LOCAL}: Local - represents a tracking space with a native origin near the
* viewer at the time of creation. It is meant for seated or basic local XR sessions.
* - {@link XRSPACE_LOCALFLOOR}: Local Floor - represents a tracking space with a native origin
* at the floor in a safe position for the user to stand. The y-axis equals 0 at floor level.
* Floor level value might be estimated by the underlying platform. It is meant for seated or
* basic local XR sessions.
* - {@link XRSPACE_BOUNDEDFLOOR}: Bounded Floor - represents a tracking space with its native
* origin at the floor, where the user is expected to move within a pre-established boundary.
* - {@link XRSPACE_UNBOUNDED}: Unbounded - represents a tracking space where the user is
* expected to move freely around their environment, potentially long distances from their
* starting point.
*
* @param {object} [options] - Object with options for XR session initialization.
* @param {string[]} [options.optionalFeatures] - Optional features for XRSession start. It is
* used for getting access to additional WebXR spec extensions.
* @param {boolean} [options.imageTracking] - Set to true to attempt to enable {@link XrImageTracking}.
* @param {boolean} [options.planeDetection] - Set to true to attempt to enable {@link XrPlaneDetection}.
* @param {XrErrorCallback} [options.callback] - Optional callback function called once the
* session is started. The callback has one argument Error - it is null if the XR session
* started successfully.
* @param {boolean} [options.anchors] - Optional boolean to attempt to enable {@link XrAnchors}.
* @param {object} [options.depthSensing] - Optional object with parameters to attempt to enable
* depth sensing.
* @param {string} [options.depthSensing.usagePreference] - Optional usage preference for depth
* sensing, can be 'cpu-optimized' or 'gpu-optimized' (XRDEPTHSENSINGUSAGE_*), defaults to
* 'cpu-optimized'. Most preferred and supported will be chosen by the underlying depth sensing
* system.
* @param {string} [options.depthSensing.dataFormatPreference] - Optional data format
* preference for depth sensing. Can be 'luminance-alpha' or 'float32' (XRDEPTHSENSINGFORMAT_*),
* defaults to 'luminance-alpha'. Most preferred and supported will be chosen by the underlying
* depth sensing system.
* @example
* // On an entity with a camera component
* this.entity.camera.startXr(pc.XRTYPE_VR, pc.XRSPACE_LOCAL, {
* callback: (err) => {
* if (err) {
* // failed to start XR session
* } else {
* // in XR
* }
* }
* });
*/ startXr(type, spaceType, options) {
this.system.app.xr.start(this, type, spaceType, options);
}
/**
* Attempt to end XR session of this camera.
*
* @param {XrErrorCallback} [callback] - Optional callback function called once session is
* ended. The callback has one argument Error - it is null if successfully ended XR session.
* @example
* // On an entity with a camera component
* this.entity.camera.endXr((err) => {
* // not anymore in XR
* });
*/ endXr(callback) {
if (!this._camera.xr) {
if (callback) callback(new Error('Camera is not in XR'));
return;
}
this._camera.xr.end(callback);
}
/**
* Function to copy properties from the source CameraComponent. Properties not copied:
* postEffects. Inherited properties not copied (all): system, entity, enabled.
*
* @param {CameraComponent} source - The source component.
* @ignore
*/ copy(source) {
this.aperture = source.aperture;
this.aspectRatio = source.aspectRatio;
this.aspectRatioMode = source.aspectRatioMode;
this.calculateProjection = source.calculateProjection;
this.calculateTransform = source.calculateTransform;
this.clearColor = source.clearColor;
this.clearColorBuffer = source.clearColorBuffer;
this.clearDepthBuffer = source.clearDepthBuffer;
this.clearStencilBuffer = source.clearStencilBuffer;
this.cullFaces = source.cullFaces;
this.disablePostEffectsLayer = source.disablePostEffectsLayer;
this.farClip = source.farClip;
this.flipFaces = source.flipFaces;
this.fov = source.fov;
this.frustumCulling = source.frustumCulling;
this.horizontalFov = source.horizontalFov;
this.layers = source.layers;
this.nearClip = source.nearClip;
this.orthoHeight = source.orthoHeight;
this.priority = source.priority;
this.projection = source.projection;
this.rect = source.rect;
this.renderTarget = source.renderTarget;
this.scissorRect = source.scissorRect;
this.sensitivity = source.sensitivity;
this.shutter = source.shutter;
}
}
export { CameraComponent };