src/components/Camera.js
import Component from '../systems/EntitySystem/Component';
import RenderSystem, { Command, RenderFullscreenCommand } from '../systems/RenderSystem';
import System from '../systems/System';
import { mat4, vec2 } from '../utils/gl-matrix';
import { getPOT, getMipmapScale } from '../utils';
const cachedTempMat4 = mat4.create();
const cachedZeroMat4 = mat4.fromValues(
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
);
let rttUidGenerator = 0;
export class PostprocessPass {
constructor() {
this._command = new RenderFullscreenCommand();
this._targets = new Map();
this._renderer = null;
}
dispose() {
this._command.dispose();
this.destroyAllTargets();
this._command = null;
this._targets = null;
this._renderer = null;
}
apply(
gl,
renderer,
textureSource,
renderTarget,
shader,
overrideUniforms = null,
overrideSamplers = null
) {
const { _command } = this;
_command.shader = shader;
_command.overrideUniforms.clear();
_command.overrideSamplers.clear();
_command.overrideSamplers.set('sBackBuffer', { texture: textureSource });
if (!!overrideUniforms) {
for (const key in overrideUniforms) {
_command.overrideUniforms.set(key, overrideUniforms[key]);
}
}
if (!!overrideSamplers) {
for (const key in overrideSamplers) {
_command.overrideSamplers.set(key, overrideSamplers[key]);
}
}
if (!renderTarget) {
renderer.disableRenderTarget();
} else {
renderer.enableRenderTarget(renderTarget);
}
_command.onRender(gl, renderer, 0, null);
}
createTarget(id, level = 0, floatPointData = false, potMode = null) {
if (typeof id !== 'string') {
throw new Error('`id` is not type of String!');
}
if (typeof level !== 'number') {
throw new Error('`level` is not type of Number!');
}
if (typeof floatPointData !== 'boolean') {
throw new Error('`floatPointData` is not type of Boolean!');
}
if (!!potMode && typeof potMode !== 'string') {
throw new Error('`potMode` is not type of String!');
}
level = level | 0;
const { _targets } = this;
if (_targets.has(id)) {
const target = _targets.get(id);
target.level = level;
target.floatPointData = floatPointData;
target.potMode = potMode;
target.dirty = true;
return target.target;
} else {
const target = `#Camera-PostprocessPass-${++rttUidGenerator}`;
_targets.set(id, {
target,
level,
floatPointData,
potMode,
dirty: true
});
return target;
}
}
destroyTarget(id) {
if (typeof id !== 'string') {
throw new Error('`id` is not type of String!');
}
const { _targets, _renderer } = this;
if (_targets.has(id)) {
const target = _targets.get(id);
if (!!_renderer) {
_renderer.unregisterRenderTarget(target.target);
}
_targets.delete(id);
}
}
destroyAllTargets() {
const { _targets, _renderer } = this;
if (!!_renderer) {
for (const target of _targets.values()) {
_renderer.unregisterRenderTarget(target.target);
}
}
_targets.clear();
}
getTargetId(id) {
if (typeof id !== 'string') {
throw new Error('`id` is not type of String!');
}
return this._targets.get(id).target || null;
}
onApply(gl, renderer, textureSource, renderTarget) {
this._renderer = renderer;
const { _targets } = this;
for (const target of _targets.values()) {
if (!!target.dirty) {
target.dirty = false;
const { width, height } = renderer.canvas;
const w = !target.potMode
? width
: getPOT(width, target.potMode === 'upper');
const h = !target.potMode
? height
: getPOT(height, target.potMode === 'upper');
const s = getMipmapScale(target.level);
renderer.registerRenderTarget(
target.target,
w * s,
h * s,
target.floatPointData
);
}
}
}
onResize(width, height) {
const { _targets } = this;
for (const target of _targets.values()) {
target.dirty = true;
}
}
}
/**
* Camera base class component.
*/
export default class Camera extends Component {
/** @type {*} */
static get propsTypes() {
return {
ignoreChildrenViews: 'boolean',
captureEntity: 'string_null',
renderTargetId: 'string_null',
renderTargetWidth: 'integer',
renderTargetHeight: 'integer',
renderTargetScale: 'number',
renderTargetFloat: 'boolean',
renderTargetMulti: 'array(any)',
layer: 'string_null'
};
}
/**
* Component factory.
*
* @return {Camera} Component instance.
*/
static factory() {
return new Camera();
}
/** @type {boolean} */
get ignoreChildrenViews() {
return this._ignoreChildrenViews;
}
/** @type {boolean} */
set ignoreChildrenViews(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean!');
}
this._ignoreChildrenViews = value;
}
/** @type {string|null} */
get captureEntity() {
return this._captureEntity;
}
/** @type {string|null} */
set captureEntity(value) {
if (typeof value !== 'string') {
throw new Error('`value` is not type of String');
}
this._captureEntity = value;
}
/** @type {string|null} */
get renderTargetId() {
return this._renderTargetId;
}
/** @type {string|null} */
set renderTargetId(value) {
if (!!value && typeof value !== 'string') {
throw new Error('`value` is not type of String');
}
this._renderTargetId = value;
this._renderTargetDirty = true;
this._dirty = true;
}
/** @type {number} */
get renderTargetWidth() {
return this._renderTargetWidth;
}
/** @type {number} */
set renderTargetWidth(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number');
}
this._renderTargetWidth = value | 0;
this._renderTargetDirty = true;
this._dirty = true;
}
/** @type {number} */
get renderTargetHeight() {
return this._renderTargetHeight;
}
/** @type {number} */
set renderTargetHeight(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number');
}
this._renderTargetHeight = value | 0;
this._renderTargetDirty = true;
this._dirty = true;
}
/** @type {number} */
get renderTargetScale() {
return this._renderTargetScale;
}
/** @type {number} */
set renderTargetScale(value) {
if (typeof value !== 'number') {
throw new Error('`value` is not type of Number');
}
this._renderTargetScale = value;
this._renderTargetDirty = true;
this._dirty = true;
}
/** @type {boolean} */
get renderTargetFloat() {
return this._renderTargetFloat;
}
/** @type {boolean} */
set renderTargetFloat(value) {
if (typeof value !== 'boolean') {
throw new Error('`value` is not type of Boolean');
}
this._renderTargetFloat = value;
this._renderTargetDirty = true;
this._dirty = true;
}
/** @type {*[]} */
get renderTargetMulti() {
return this._renderTargetMulti;
}
/** @type {*[]} */
set renderTargetMulti(value) {
this._renderTargetMulti = value;
this._renderTargetDirty = true;
this._dirty = true;
}
/** @type {string|null} */
get layer() {
return this._layer;
}
/** @type {string|null} */
set layer(value) {
if (!!value && typeof value !== 'string') {
throw new Error('`value` is not type of String!');
}
this._layer = value;
}
/** @type {mat4} */
get projectionMatrix() {
return this._projectionMatrix;
}
/** @type {mat4} */
get inverseProjectionMatrix() {
return this._inverseProjectionMatrix;
}
/** @type {mat4} */
get viewMatrix() {
return this.entity.transform;
}
/** @type {mat4} */
get inverseViewMatrix() {
return this.entity.inverseTransform;
}
/** @type {mat4} */
get viewProjectionMatrix() {
return this.entity.transform;
}
/** @type {mat4} */
get inverseViewProjectionMatrix() {
return this.entity.inverseTransform;
}
/** @type {Command|null} */
get command() {
return this._command;
}
/** @type {Command|null} */
set command(value) {
if (!value) {
this._command = null;
return;
}
if (!(value instanceof Command)) {
throw new Error('`value` is not type of Command!');
}
this._command = value;
}
/**
* Constructor.
*/
constructor() {
super();
this._ignoreChildrenViews = false;
this._captureEntity = null;
this._projectionMatrix = mat4.create();
this._inverseProjectionMatrix = mat4.create();
this._viewProjectionMatrix = mat4.create();
this._inverseViewProjectionMatrix = mat4.create();
mat4.copy(this._projectionMatrix, cachedZeroMat4);
mat4.copy(this._inverseProjectionMatrix, cachedZeroMat4);
mat4.copy(this._viewProjectionMatrix, cachedZeroMat4);
mat4.copy(this._inverseViewProjectionMatrix, cachedZeroMat4);
this._context = null;
this._renderTargetId = null;
this._renderTargetIdUsed = null;
this._renderTargetWidth = 0;
this._renderTargetHeight = 0;
this._renderTargetScale = 1;
this._renderTargetFloat = false;
this._renderTargetMulti = null;
this._renderTargetDirty = false;
this._layer = null;
this._postprocess = null;
this._postprocessRtt = null;
this._postprocessCachedWidth = 0;
this._postprocessCachedHeight = 0;
this._command = null;
this._dirty = true;
this._onResize = this.onResize.bind(this);
}
/**
* @override
*/
dispose() {
super.dispose();
const {
_context,
_renderTargetIdUsed,
_postprocessRtt,
_command
} = this;
if (!!_context) {
if (!!_renderTargetIdUsed) {
_context.unregisterRenderTarget(_renderTargetIdUsed);
}
if (!!_postprocessRtt) {
_context.unregisterRenderTarget(_postprocessRtt);
}
this._context = null;
}
if (!!_command) {
_command.dispose();
}
this._captureEntity = null;
this._projectionMatrix = null;
this._inverseProjectionMatrix = null;
this._viewProjectionMatrix = null;
this._inverseViewProjectionMatrix = null;
this._postprocess = null;
this._renderTargetId = null;
this._renderTargetIdUsed = null;
this._renderTargetMulti = null;
this._postprocessRtt = null;
this._layer = null;
this._postprocess = null;
this._postprocessRtt = null;
this._command = null;
this._onResize = null;
}
/**
* Building camera matrix.
*
* @abstract
* @param {mat4} target - Result mat4 object.
* @param {number} width - Width.
* @param {number} height - Height.
*/
buildCameraMatrix(target, width, height) {
throw new Error('Not implemented!');
}
/**
* Register postprocess.
*
* @param {PostprocessPass} postprocess - Postprocess pass.
* @param {boolean} floatPointData - Tells if stores floating point texture data.
*/
registerPostprocess(postprocess, floatPointData = false) {
if (!(postprocess instanceof PostprocessPass)) {
throw new Error('`postprocess` is not type of PostprocessPass!');
}
const { _postprocess } = this;
if (!_postprocess) {
this._postprocessCachedWidth = 0;
this._postprocessCachedHeight = 0;
}
this._postprocess = {
postprocess,
floatPointData
};
}
/**
* Unregister postprocess.
*/
unregisterPostprocess() {
const { _postprocess } = this;
if (!_postprocess) {
return;
}
this._postprocess = null;
this._postprocessCachedWidth = 0;
this._postprocessCachedHeight = 0;
}
/**
* Convert screen space unit ([-1; 1]) vec2 point to global vec2 point.
*
* @param {vec2} target - Result vec2 point.
* @param {vec2} unitVec - Input screen space unit vec2 point.
*/
convertUnitPointToGlobalPoint(target, unitVec) {
vec2.transformMat4(
target,
unitVec,
this._inverseViewProjectionMatrix
);
}
/**
* @override
*/
onAttach() {
const { RenderSystem } = System.systems;
if (!RenderSystem) {
throw new Error('There is no registered RenderSystem!');
}
RenderSystem.events.on('resize', this._onResize);
}
/**
* @override
*/
onDetach() {
const { RenderSystem } = System.systems;
if (!RenderSystem) {
throw new Error('There is no registered RenderSystem!');
}
RenderSystem.events.off('resize', this._onResize);
}
/**
* @override
*/
onAction(name, ...args) {
if (name === 'view') {
return this.onView(...args);
}
}
/**
* Called when camera need to view rendered scene.
*
* @param {WebGLRenderingContext} gl - WebGL context.
* @param {RenderSystem} renderer - Calling renderer instance.
* @param {number} deltaTime - Delta time.
*
* @return {boolean} True if ignore viewing entity children, false otherwise.
*/
onView(gl, renderer, deltaTime) {
const { entity, _ignoreChildrenViews } = this;
if (!entity) {
return _ignoreChildrenViews;
}
let { width, height } = renderer.canvas;
const {
_captureEntity,
_projectionMatrix,
_inverseProjectionMatrix,
_viewProjectionMatrix,
_inverseViewProjectionMatrix,
_renderTargetWidth,
_renderTargetHeight,
_renderTargetScale,
_postprocess
} = this;
if (_renderTargetWidth > 0) {
width = _renderTargetWidth;
}
if (_renderTargetHeight > 0) {
height = _renderTargetHeight;
}
const target = !!_captureEntity
? entity.findEntity(_captureEntity)
: entity;
if ((width | 0) === 0 || (height | 0) === 0) {
mat4.copy(_projectionMatrix, cachedZeroMat4);
mat4.copy(_inverseProjectionMatrix, cachedZeroMat4);
mat4.copy(renderer.projectionMatrix, cachedZeroMat4);
mat4.copy(renderer.viewMatrix, cachedZeroMat4);
mat4.copy(_viewProjectionMatrix, cachedZeroMat4);
mat4.copy(_inverseViewProjectionMatrix, cachedZeroMat4);
if (this._dirty) {
this._dirty = false;
target.performAction('camera-changed', this);
}
return _ignoreChildrenViews;
}
this._context = renderer;
if (this._renderTargetDirty) {
if (!!this._renderTargetId) {
if (!!this._renderTargetIdUsed) {
renderer.unregisterRenderTarget(this._renderTargetIdUsed);
}
this._renderTargetIdUsed = this._renderTargetId;
if (!!this._renderTargetMulti) {
renderer.registerRenderTargetMulti(
this._renderTargetIdUsed,
width * _renderTargetScale,
height * _renderTargetScale,
this._renderTargetMulti
);
} else {
renderer.registerRenderTarget(
this._renderTargetIdUsed,
width * _renderTargetScale,
height * _renderTargetScale,
this._renderTargetFloat
);
}
} else {
renderer.unregisterRenderTarget(this._renderTargetIdUsed);
this._renderTargetIdUsed = null;
}
this._renderTargetDirty = false;
}
this.buildCameraMatrix(_projectionMatrix, width, height);
mat4.invert(_inverseProjectionMatrix, _projectionMatrix);
mat4.copy(renderer.projectionMatrix, _projectionMatrix);
mat4.copy(renderer.viewMatrix, entity.inverseTransform);
mat4.multiply(
_viewProjectionMatrix,
_projectionMatrix,
entity.inverseTransform
);
mat4.invert(_inverseViewProjectionMatrix, _viewProjectionMatrix);
if (this._postprocessCachedWidth !== width ||
this._postprocessCachedHeight !== height
) {
this._postprocessCachedWidth = width;
this._postprocessCachedHeight = height;
if (!!this._postprocessRtt) {
renderer.unregisterRenderTarget(this._postprocessRtt);
this._postprocessRtt = null;
}
if (!!_postprocess) {
const rtt = `#Camera-PostprocessPass-${++rttUidGenerator}`;
renderer.registerRenderTarget(
rtt,
width,
height,
_postprocess.floatPointData
);
this._postprocessRtt = rtt;
}
}
if (!_postprocess) {
if (!!this._renderTargetIdUsed) {
renderer.enableRenderTarget(this._renderTargetIdUsed);
}
if (!!this._command) {
renderer.executeCommand(this._command, deltaTime, this._layer);
} else {
if (!!this._layer) {
target.performAction(
'render-layer',
gl,
renderer,
deltaTime,
this._layer
);
} else {
target.performAction('render', gl, renderer, deltaTime, null);
}
}
if (!!this._renderTargetIdUsed) {
renderer.disableRenderTarget();
}
} else {
const { _postprocessRtt } = this;
renderer.enableRenderTarget(_postprocessRtt);
if (!!this._command) {
renderer.executeCommand(this._command, deltaTime, this._layer);
} else {
if (!!this._layer) {
target.performAction(
'render-layer',
gl,
renderer,
deltaTime,
this._layer
);
} else {
target.performAction('render', gl, renderer, deltaTime, null);
}
}
_postprocess.postprocess.onApply(
gl,
renderer,
_postprocessRtt,
this._renderTargetIdUsed || null
);
renderer.disableRenderTarget();
}
if (this._dirty) {
this._dirty = false;
target.performAction('camera-changed', this);
}
return _ignoreChildrenViews;
}
/**
* Called on view resize.
*
* @param {number} width - Width.
* @param {number} height - Height.
*/
onResize(width, height) {
const {
_renderTargetWidth,
_renderTargetHeight,
_command,
_postprocess
} = this;
if (_renderTargetWidth <= 0 || _renderTargetHeight <= 0) {
this._renderTargetDirty = true;
this._postprocessCachedWidth = 0;
this._postprocessCachedHeight = 0;
this._dirty = true;
}
if (!!_command) {
_command.onResize(width, height);
}
if (!!_postprocess) {
_postprocess.postprocess.onResize(width, height);
}
}
}