UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

635 lines (632 loc) 23.5 kB
import { Color } from '../core/math/color.js'; import { Mat4 } from '../core/math/mat4.js'; import { Vec3 } from '../core/math/vec3.js'; import { Vec4 } from '../core/math/vec4.js'; import { math } from '../core/math/math.js'; import { Frustum } from '../core/shape/frustum.js'; import { PROJECTION_PERSPECTIVE, ASPECT_AUTO, LAYERID_WORLD, LAYERID_DEPTH, LAYERID_SKYBOX, LAYERID_UI, LAYERID_IMMEDIATE } from './constants.js'; import { RenderPassColorGrab } from './graphics/render-pass-color-grab.js'; import { RenderPassDepthGrab } from './graphics/render-pass-depth-grab.js'; import { CameraShaderParams } from './camera-shader-params.js'; /** * @import { RenderPass } from '../platform/graphics/render-pass.js' * @import { FogParams } from './fog-params.js' * @import { ShaderPassInfo } from './shader-pass.js' */ // pre-allocated temp variables var _deviceCoord = new Vec3(); var _halfSize = new Vec3(); var _point = new Vec3(); var _invViewProjMat = new Mat4(); var _frustumPoints = [ new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3(), new Vec3() ]; /** * A camera. * * @ignore */ class Camera { destroy() { var _this_renderPassColorGrab, _this_renderPassDepthGrab; (_this_renderPassColorGrab = this.renderPassColorGrab) == null ? undefined : _this_renderPassColorGrab.destroy(); this.renderPassColorGrab = null; (_this_renderPassDepthGrab = this.renderPassDepthGrab) == null ? undefined : _this_renderPassDepthGrab.destroy(); this.renderPassDepthGrab = null; this.renderPasses.length = 0; } /** * Store camera matrices required by TAA. Only update them once per frame. */ _storeShaderMatrices(viewProjMat, jitterX, jitterY, renderVersion) { if (this._shaderMatricesVersion !== renderVersion) { this._shaderMatricesVersion = renderVersion; var _this__viewProjCurrent; this._viewProjPrevious.copy((_this__viewProjCurrent = this._viewProjCurrent) != null ? _this__viewProjCurrent : viewProjMat); var _this__viewProjCurrent1; (_this__viewProjCurrent1 = this._viewProjCurrent) != null ? _this__viewProjCurrent1 : this._viewProjCurrent = new Mat4(); this._viewProjCurrent.copy(viewProjMat); this._viewProjInverse.invert(viewProjMat); this._jitters[2] = this._jitters[0]; this._jitters[3] = this._jitters[1]; this._jitters[0] = jitterX; this._jitters[1] = jitterY; } } /** * True if the camera clears the full render target. (viewport / scissor are full size) */ get fullSizeClearRect() { var rect = this._scissorRectClear ? this.scissorRect : this._rect; return rect.x === 0 && rect.y === 0 && rect.z === 1 && rect.w === 1; } set aspectRatio(newValue) { if (this._aspectRatio !== newValue) { this._aspectRatio = newValue; this._projMatDirty = true; } } get aspectRatio() { var _this_xr; return ((_this_xr = this.xr) == null ? undefined : _this_xr.active) ? this._xrProperties.aspectRatio : this._aspectRatio; } set aspectRatioMode(newValue) { if (this._aspectRatioMode !== newValue) { this._aspectRatioMode = newValue; this._projMatDirty = true; } } get aspectRatioMode() { return this._aspectRatioMode; } set calculateProjection(newValue) { this._calculateProjection = newValue; this._projMatDirty = true; } get calculateProjection() { return this._calculateProjection; } set calculateTransform(newValue) { this._calculateTransform = newValue; } get calculateTransform() { return this._calculateTransform; } set clearColor(newValue) { this._clearColor.copy(newValue); } get clearColor() { return this._clearColor; } set clearColorBuffer(newValue) { this._clearColorBuffer = newValue; } get clearColorBuffer() { return this._clearColorBuffer; } set clearDepth(newValue) { this._clearDepth = newValue; } get clearDepth() { return this._clearDepth; } set clearDepthBuffer(newValue) { this._clearDepthBuffer = newValue; } get clearDepthBuffer() { return this._clearDepthBuffer; } set clearStencil(newValue) { this._clearStencil = newValue; } get clearStencil() { return this._clearStencil; } set clearStencilBuffer(newValue) { this._clearStencilBuffer = newValue; } get clearStencilBuffer() { return this._clearStencilBuffer; } set cullFaces(newValue) { this._cullFaces = newValue; } get cullFaces() { return this._cullFaces; } set farClip(newValue) { if (this._farClip !== newValue) { this._farClip = newValue; this._projMatDirty = true; } } get farClip() { var _this_xr; return ((_this_xr = this.xr) == null ? undefined : _this_xr.active) ? this._xrProperties.farClip : this._farClip; } set flipFaces(newValue) { this._flipFaces = newValue; } get flipFaces() { return this._flipFaces; } set fov(newValue) { if (this._fov !== newValue) { this._fov = newValue; this._projMatDirty = true; } } get fov() { var _this_xr; return ((_this_xr = this.xr) == null ? undefined : _this_xr.active) ? this._xrProperties.fov : this._fov; } set frustumCulling(newValue) { this._frustumCulling = newValue; } get frustumCulling() { return this._frustumCulling; } set horizontalFov(newValue) { if (this._horizontalFov !== newValue) { this._horizontalFov = newValue; this._projMatDirty = true; } } get horizontalFov() { var _this_xr; return ((_this_xr = this.xr) == null ? undefined : _this_xr.active) ? this._xrProperties.horizontalFov : this._horizontalFov; } set layers(newValue) { this._layers = newValue.slice(0); this._layersSet = new Set(this._layers); } get layers() { return this._layers; } get layersSet() { return this._layersSet; } set nearClip(newValue) { if (this._nearClip !== newValue) { this._nearClip = newValue; this._projMatDirty = true; } } get nearClip() { var _this_xr; return ((_this_xr = this.xr) == null ? undefined : _this_xr.active) ? this._xrProperties.nearClip : this._nearClip; } set node(newValue) { this._node = newValue; } get node() { return this._node; } set orthoHeight(newValue) { if (this._orthoHeight !== newValue) { this._orthoHeight = newValue; this._projMatDirty = true; } } get orthoHeight() { return this._orthoHeight; } set projection(newValue) { if (this._projection !== newValue) { this._projection = newValue; this._projMatDirty = true; } } get projection() { return this._projection; } get projectionMatrix() { this._evaluateProjectionMatrix(); return this._projMat; } set rect(newValue) { this._rect.copy(newValue); } get rect() { return this._rect; } set renderTarget(newValue) { this._renderTarget = newValue; } get renderTarget() { return this._renderTarget; } set scissorRect(newValue) { this._scissorRect.copy(newValue); } get scissorRect() { return this._scissorRect; } get viewMatrix() { if (this._viewMatDirty) { var wtm = this._node.getWorldTransform(); this._viewMat.copy(wtm).invert(); this._viewMatDirty = false; } return this._viewMat; } set aperture(newValue) { this._aperture = newValue; } get aperture() { return this._aperture; } set sensitivity(newValue) { this._sensitivity = newValue; } get sensitivity() { return this._sensitivity; } set shutter(newValue) { this._shutter = newValue; } get shutter() { return this._shutter; } set xr(newValue) { if (this._xr !== newValue) { this._xr = newValue; this._projMatDirty = true; } } get xr() { return this._xr; } /** * Creates a duplicate of the camera. * * @returns {Camera} A cloned Camera. */ clone() { return new Camera().copy(this); } /** * Copies one camera to another. * * @param {Camera} other - Camera to copy. * @returns {Camera} Self for chaining. */ copy(other) { // We aren't using the getters and setters because there is additional logic // around using WebXR in the getters for these properties so that functions // like screenToWorld work correctly with other systems like the UI input // system this._aspectRatio = other._aspectRatio; this._farClip = other._farClip; this._fov = other._fov; this._horizontalFov = other._horizontalFov; this._nearClip = other._nearClip; this._xrProperties.aspectRatio = other._xrProperties.aspectRatio; this._xrProperties.farClip = other._xrProperties.farClip; this._xrProperties.fov = other._xrProperties.fov; this._xrProperties.horizontalFov = other._xrProperties.horizontalFov; this._xrProperties.nearClip = other._xrProperties.nearClip; this.aspectRatioMode = other.aspectRatioMode; this.calculateProjection = other.calculateProjection; this.calculateTransform = other.calculateTransform; this.clearColor = other.clearColor; this.clearColorBuffer = other.clearColorBuffer; this.clearDepth = other.clearDepth; this.clearDepthBuffer = other.clearDepthBuffer; this.clearStencil = other.clearStencil; this.clearStencilBuffer = other.clearStencilBuffer; this.cullFaces = other.cullFaces; this.flipFaces = other.flipFaces; this.frustumCulling = other.frustumCulling; this.layers = other.layers; this.orthoHeight = other.orthoHeight; this.projection = other.projection; this.rect = other.rect; this.renderTarget = other.renderTarget; this.scissorRect = other.scissorRect; this.aperture = other.aperture; this.shutter = other.shutter; this.sensitivity = other.sensitivity; this.shaderPassInfo = other.shaderPassInfo; this.jitter = other.jitter; this._projMatDirty = true; return this; } _enableRenderPassColorGrab(device, enable) { if (enable) { if (!this.renderPassColorGrab) { this.renderPassColorGrab = new RenderPassColorGrab(device); } } else { var _this_renderPassColorGrab; (_this_renderPassColorGrab = this.renderPassColorGrab) == null ? undefined : _this_renderPassColorGrab.destroy(); this.renderPassColorGrab = null; } } _enableRenderPassDepthGrab(device, renderer, enable) { if (enable) { if (!this.renderPassDepthGrab) { this.renderPassDepthGrab = new RenderPassDepthGrab(device, this); } } else { var _this_renderPassDepthGrab; (_this_renderPassDepthGrab = this.renderPassDepthGrab) == null ? undefined : _this_renderPassDepthGrab.destroy(); this.renderPassDepthGrab = null; } } _updateViewProjMat() { if (this._projMatDirty || this._viewMatDirty || this._viewProjMatDirty) { this._viewProjMat.mul2(this.projectionMatrix, this.viewMatrix); this._viewProjMatDirty = false; } } /** * Convert a point from 3D world space to 2D canvas pixel space. * * @param {Vec3} worldCoord - The world space coordinate to transform. * @param {number} cw - The width of PlayCanvas' canvas element. * @param {number} ch - The height of PlayCanvas' canvas element. * @param {Vec3} [screenCoord] - 3D vector to receive screen coordinate result. * @returns {Vec3} The screen space coordinate. */ worldToScreen(worldCoord, cw, ch, screenCoord) { if (screenCoord === undefined) screenCoord = new Vec3(); this._updateViewProjMat(); this._viewProjMat.transformPoint(worldCoord, screenCoord); // calculate w co-coord var vpm = this._viewProjMat.data; var w = worldCoord.x * vpm[3] + worldCoord.y * vpm[7] + worldCoord.z * vpm[11] + 1 * vpm[15]; screenCoord.x = (screenCoord.x / w + 1) * 0.5 * cw; screenCoord.y = (1 - screenCoord.y / w) * 0.5 * ch; return screenCoord; } /** * Convert a point from 2D canvas pixel space to 3D world space. * * @param {number} x - X coordinate on PlayCanvas' canvas element. * @param {number} y - Y coordinate on PlayCanvas' canvas element. * @param {number} z - The distance from the camera in world space to create the new point. * @param {number} cw - The width of PlayCanvas' canvas element. * @param {number} ch - The height of PlayCanvas' canvas element. * @param {Vec3} [worldCoord] - 3D vector to receive world coordinate result. * @returns {Vec3} The world space coordinate. */ screenToWorld(x, y, z, cw, ch, worldCoord) { if (worldCoord === undefined) worldCoord = new Vec3(); // Calculate the screen click as a point on the far plane of the normalized device coordinate 'box' (z=1) var range = this.farClip - this.nearClip; _deviceCoord.set(x / cw, (ch - y) / ch, z / range); _deviceCoord.mulScalar(2); _deviceCoord.sub(Vec3.ONE); if (this._projection === PROJECTION_PERSPECTIVE) { // calculate half width and height at the near clip plane Mat4._getPerspectiveHalfSize(_halfSize, this.fov, this.aspectRatio, this.nearClip, this.horizontalFov); // scale by normalized screen coordinates _halfSize.x *= _deviceCoord.x; _halfSize.y *= _deviceCoord.y; // transform to world space var invView = this._node.getWorldTransform(); _halfSize.z = -this.nearClip; invView.transformPoint(_halfSize, _point); // point along camera->_point ray at distance z from the camera var cameraPos = this._node.getPosition(); worldCoord.sub2(_point, cameraPos); worldCoord.normalize(); worldCoord.mulScalar(z); worldCoord.add(cameraPos); } else { this._updateViewProjMat(); _invViewProjMat.copy(this._viewProjMat).invert(); // Transform to world space _invViewProjMat.transformPoint(_deviceCoord, worldCoord); } return worldCoord; } _evaluateProjectionMatrix() { if (this._projMatDirty) { if (this._projection === PROJECTION_PERSPECTIVE) { this._projMat.setPerspective(this.fov, this.aspectRatio, this.nearClip, this.farClip, this.horizontalFov); this._projMatSkybox.copy(this._projMat); } else { var y = this._orthoHeight; var x = y * this.aspectRatio; this._projMat.setOrtho(-x, x, -y, y, this.nearClip, this.farClip); this._projMatSkybox.setPerspective(this.fov, this.aspectRatio, this.nearClip, this.farClip); } this._projMatDirty = false; } } getProjectionMatrixSkybox() { this._evaluateProjectionMatrix(); return this._projMatSkybox; } getExposure() { var ev100 = Math.log2(this._aperture * this._aperture / this._shutter * 100.0 / this._sensitivity); return 1.0 / (Math.pow(2.0, ev100) * 1.2); } // returns estimated size of the sphere on the screen in range of [0..1] // 0 - infinitely small, 1 - full screen or larger getScreenSize(sphere) { if (this._projection === PROJECTION_PERSPECTIVE) { // camera to sphere distance var distance = this._node.getPosition().distance(sphere.center); // if we're inside the sphere if (distance < sphere.radius) { return 1; } // The view-angle of the bounding sphere rendered on screen var viewAngle = Math.asin(sphere.radius / distance); // This assumes the near clipping plane is at a distance of 1 var sphereViewHeight = Math.tan(viewAngle); // The size of (half) the screen if the near clipping plane is at a distance of 1 var screenViewHeight = Math.tan(this.fov / 2 * math.DEG_TO_RAD); // The ratio of the geometry's screen size compared to the actual size of the screen return Math.min(sphereViewHeight / screenViewHeight, 1); } // ortho return math.clamp(sphere.radius / this._orthoHeight, 0, 1); } /** * Returns an array of corners of the frustum of the camera in the local coordinate system of the camera. * * @param {number} [near] - Near distance for the frustum points. Defaults to the near clip distance of the camera. * @param {number} [far] - Far distance for the frustum points. Defaults to the far clip distance of the camera. * @returns {Vec3[]} - An array of corners, using a global storage space. */ getFrustumCorners(near, far) { if (near === undefined) near = this.nearClip; if (far === undefined) far = this.farClip; var fov = this.fov * Math.PI / 180.0; var x, y; if (this.projection === PROJECTION_PERSPECTIVE) { if (this.horizontalFov) { x = near * Math.tan(fov / 2.0); y = x / this.aspectRatio; } else { y = near * Math.tan(fov / 2.0); x = y * this.aspectRatio; } } else { y = this._orthoHeight; x = y * this.aspectRatio; } var points = _frustumPoints; points[0].x = x; points[0].y = -y; points[0].z = -near; points[1].x = x; points[1].y = y; points[1].z = -near; points[2].x = -x; points[2].y = y; points[2].z = -near; points[3].x = -x; points[3].y = -y; points[3].z = -near; if (this._projection === PROJECTION_PERSPECTIVE) { if (this.horizontalFov) { x = far * Math.tan(fov / 2.0); y = x / this.aspectRatio; } else { y = far * Math.tan(fov / 2.0); x = y * this.aspectRatio; } } points[4].x = x; points[4].y = -y; points[4].z = -far; points[5].x = x; points[5].y = y; points[5].z = -far; points[6].x = -x; points[6].y = y; points[6].z = -far; points[7].x = -x; points[7].y = -y; points[7].z = -far; return points; } /** * Sets XR camera properties that should be derived physical camera in {@link XrManager}. * * @param {object} [properties] - Properties object. * @param {number} [properties.aspectRatio] - Aspect ratio. * @param {number} [properties.farClip] - Far clip. * @param {number} [properties.fov] - Field of view. * @param {boolean} [properties.horizontalFov] - Enable horizontal field of view. * @param {number} [properties.nearClip] - Near clip. */ setXrProperties(properties) { Object.assign(this._xrProperties, properties); this._projMatDirty = true; } constructor(){ /** * @type {ShaderPassInfo|null} */ this.shaderPassInfo = null; /** * @type {RenderPassColorGrab|null} */ this.renderPassColorGrab = null; /** * @type {RenderPass|null} */ this.renderPassDepthGrab = null; /** * The fog parameters. * * @type {FogParams|null} */ this.fogParams = null; /** * Shader parameters used to generate and use matching shaders. * * @type {CameraShaderParams} */ this.shaderParams = new CameraShaderParams(); /** * Render passes used to render this camera. If empty, the camera will render using the default * render passes. * * @type {RenderPass[]} */ this.renderPasses = []; /** @type {number} */ this.jitter = 0; this._aspectRatio = 16 / 9; this._aspectRatioMode = ASPECT_AUTO; this._calculateProjection = null; this._calculateTransform = null; this._clearColor = new Color(0.75, 0.75, 0.75, 1); this._clearColorBuffer = true; this._clearDepth = 1; this._clearDepthBuffer = true; this._clearStencil = 0; this._clearStencilBuffer = true; this._cullFaces = true; this._farClip = 1000; this._flipFaces = false; this._fov = 45; this._frustumCulling = true; this._horizontalFov = false; this._layers = [ LAYERID_WORLD, LAYERID_DEPTH, LAYERID_SKYBOX, LAYERID_UI, LAYERID_IMMEDIATE ]; this._layersSet = new Set(this._layers); this._nearClip = 0.1; this._node = null; this._orthoHeight = 10; this._projection = PROJECTION_PERSPECTIVE; this._rect = new Vec4(0, 0, 1, 1); this._renderTarget = null; this._scissorRect = new Vec4(0, 0, 1, 1); this._scissorRectClear = false; // by default rect is used when clearing. this allows scissorRect to be used when clearing. this._aperture = 16.0; this._shutter = 1.0 / 1000.0; this._sensitivity = 1000; this._projMat = new Mat4(); this._projMatDirty = true; this._projMatSkybox = new Mat4(); // projection matrix used by skybox rendering shader is always perspective this._viewMat = new Mat4(); this._viewMatDirty = true; this._viewProjMat = new Mat4(); this._viewProjMatDirty = true; // storage of actual matrices used by the shaders, needed by TAA this._shaderMatricesVersion = 0; this._viewProjInverse = new Mat4(); // inverse view projection matrix from the current frame this._viewProjCurrent = null; // view projection matrix from the current frame this._viewProjPrevious = new Mat4(); // view projection matrix from the previous frame this._jitters = [ 0, 0, 0, 0 ]; // jitter values for TAA, 0-1 - current frame, 2-3 - previous frame this.frustum = new Frustum(); // Set by XrManager this._xr = null; this._xrProperties = { horizontalFov: this._horizontalFov, fov: this._fov, aspectRatio: this._aspectRatio, farClip: this._farClip, nearClip: this._nearClip }; } } export { Camera };