pex-renderer
Version:
Physically Based Renderer (PBR) and scene graph designed as ECS for PEX: define entities to be rendered as collections of components with their update orchestrated by systems.
359 lines (323 loc) • 12.5 kB
JavaScript
import { mat4, vec3, quat, utils, avec4 } from "pex-math";
import { orbiter as createOrbiter } from "pex-cam";
import { NAMESPACE, TEMP_MAT4, TEMP_VEC3 } from "../utils.js";
function computeFrustum(camera) {
mat4.set(TEMP_MAT4, camera.projectionMatrix);
mat4.mult(TEMP_MAT4, camera.viewMatrix);
// prettier-ignore
{
avec4.set4(camera.frustum, 0, TEMP_MAT4[3] - TEMP_MAT4[0], TEMP_MAT4[7] - TEMP_MAT4[4], TEMP_MAT4[11] - TEMP_MAT4[8], TEMP_MAT4[15] - TEMP_MAT4[12]) // -x
avec4.set4(camera.frustum, 1, TEMP_MAT4[3] + TEMP_MAT4[0], TEMP_MAT4[7] + TEMP_MAT4[4], TEMP_MAT4[11] + TEMP_MAT4[8], TEMP_MAT4[15] + TEMP_MAT4[12]) // +x
avec4.set4(camera.frustum, 2, TEMP_MAT4[3] + TEMP_MAT4[1], TEMP_MAT4[7] + TEMP_MAT4[5], TEMP_MAT4[11] + TEMP_MAT4[9], TEMP_MAT4[15] + TEMP_MAT4[13]) // +y
avec4.set4(camera.frustum, 3, TEMP_MAT4[3] - TEMP_MAT4[1], TEMP_MAT4[7] - TEMP_MAT4[5], TEMP_MAT4[11] - TEMP_MAT4[9], TEMP_MAT4[15] - TEMP_MAT4[13]) // -y
avec4.set4(camera.frustum, 4, TEMP_MAT4[3] - TEMP_MAT4[2], TEMP_MAT4[7] - TEMP_MAT4[6], TEMP_MAT4[11] - TEMP_MAT4[10], TEMP_MAT4[15] - TEMP_MAT4[14]) // +z (far)
avec4.set4(camera.frustum, 5, TEMP_MAT4[3] + TEMP_MAT4[2], TEMP_MAT4[7] + TEMP_MAT4[6], TEMP_MAT4[11] + TEMP_MAT4[10], TEMP_MAT4[15] + TEMP_MAT4[14]) // -z (near)
}
// Normalize planes
for (let i = 0; i < 6; i++) {
TEMP_VEC3[0] = camera.frustum[i * 4];
TEMP_VEC3[1] = camera.frustum[i * 4 + 1];
TEMP_VEC3[2] = camera.frustum[i * 4 + 2];
avec4.scale(camera.frustum, i, vec3.length(TEMP_VEC3));
}
}
// TODO: projectionMatrix should only be recomputed if parameters changed
function updateCameraProjection(camera, transform) {
if (camera.projection === "orthographic") {
const dx = (camera.right - camera.left) / (2 / camera.zoom);
const dy = (camera.top - camera.bottom) / (2 / camera.zoom);
const cx = (camera.right + camera.left) / 2;
const cy = (camera.top + camera.bottom) / 2;
let left = cx - dx;
let right = cx + dx;
let top = cy + dy;
let bottom = cy - dy;
if (camera.view) {
const zoomW =
1 / camera.zoom / (camera.view.size[0] / camera.view.totalSize[0]);
const zoomH =
1 / camera.zoom / (camera.view.size[1] / camera.view.totalSize[1]);
const scaleW = (camera.right - camera.left) / camera.view.size[0];
const scaleH = (camera.top - camera.bottom) / camera.view.size[1];
left += scaleW * (camera.view.offset[0] / zoomW);
right = left + scaleW * (camera.view.size[0] / zoomW);
top -= scaleH * (camera.view.offset[1] / zoomH);
bottom = top - scaleH * (camera.view.size[1] / zoomH);
}
mat4.ortho(
camera.projectionMatrix,
left,
right,
bottom,
top,
camera.near,
camera.far,
);
} else {
if (camera.view) {
const aspectRatio = camera.view.totalSize[0] / camera.view.totalSize[1];
const top = Math.tan(camera.fov * 0.5) * camera.near;
const bottom = -top;
const left = aspectRatio * bottom;
const right = aspectRatio * top;
const width = Math.abs(right - left);
const height = Math.abs(top - bottom);
const widthNormalized = width / camera.view.totalSize[0];
const heightNormalized = height / camera.view.totalSize[1];
const l = left + camera.view.offset[0] * widthNormalized;
const r =
left + (camera.view.offset[0] + camera.view.size[0]) * widthNormalized;
const b =
top - (camera.view.offset[1] + camera.view.size[1]) * heightNormalized;
const t = top - camera.view.offset[1] * heightNormalized;
mat4.frustum(
camera.projectionMatrix,
l,
r,
b,
t,
camera.near,
camera.far,
);
} else {
mat4.perspective(
camera.projectionMatrix,
camera.fov,
camera.aspect,
camera.near,
camera.far,
);
}
}
mat4.set(camera.invViewMatrix, transform.modelMatrix);
//look at matrix is opposite of camera modelMatrix transform
mat4.set(camera.viewMatrix, transform.modelMatrix);
mat4.invert(camera.viewMatrix);
if (camera.culling) computeFrustum(camera);
}
/**
* Camera system
*
* Adds:
* - "_orbiter" to orbiter components
* @returns {import("../types.js").System}
* @alias module:systems.camera
*/
export default () => ({
type: "camera-system",
cache: {},
debug: false,
updateCameraProjection,
computeFrustum,
checkCamera(_, cameraEntity) {
if (!cameraEntity.transform) {
console.warn(
NAMESPACE,
this.type,
`camera entity missing transform. Add a transformSystem.update(entities).`,
);
} else {
return true;
}
},
updateCameraFoV(entity) {
const camera = entity.camera;
let sensorWidth = camera.sensorSize[0];
let sensorHeight = camera.sensorSize[1];
const sensorAspectRatio = sensorWidth / sensorHeight;
if (camera.aspect > sensorAspectRatio) {
if (camera.sensorFit === "horizontal" || camera.sensorFit === "fill") {
sensorHeight = sensorWidth / camera.aspect;
}
} else {
if (
camera.sensorFit === "horizontal" ||
camera.sensorFit === "overscan"
) {
sensorHeight = sensorWidth / camera.aspect;
}
}
camera.actualSensorHeight = sensorHeight;
if (this.cache[entity.id].fov !== camera.fov) {
camera.focalLength = sensorHeight / 2 / Math.tan(camera.fov / 2);
this.cache[entity.id].fov = camera.fov;
this.cache[entity.id].focalLength = camera.focalLength;
} else if (this.cache[entity.id].focalLength !== camera.focalLength) {
camera.fov = 2 * Math.atan(sensorHeight / 2 / camera.focalLength);
this.cache[entity.id].fov = camera.fov;
this.cache[entity.id].focalLength = camera.focalLength;
}
},
updateCameraEntity(entity) {
const orbiter = entity.orbiter;
const camera = entity.camera;
// Add to cache and reset cache if camera component is different
if (this.cache[entity.id]?.camera !== camera) {
this.cache[entity.id] = { camera };
}
this.updateCameraFoV(entity);
if (orbiter) {
if (!orbiter._orbiter) {
updateCameraProjection(entity.camera, entity._transform);
const proxyCamera = {
viewMatrix: camera.viewMatrix,
invViewMatrix: camera.invViewMatrix,
position: [...entity.transform.position],
rotationCache: [...entity.transform.rotation],
target: [...orbiter.target],
up: [0, 1, 0],
zoom: camera.zoom,
getViewRay: (x, y, windowWidth, windowHeight) => {
let nx = (2 * x) / windowWidth - 1;
let ny = 1 - (2 * y) / windowHeight;
const hNear = 2 * Math.tan(camera.fov / 2) * camera.near;
const wNear = hNear * camera.aspect;
nx *= wNear * 0.5;
ny *= hNear * 0.5; // [origin, direction]
return [[0, 0, 0], vec3.normalize([nx, ny, -camera.near])];
},
set({ target, position, zoom }) {
if (zoom) {
camera.zoom = zoom;
return;
}
if (target) {
vec3.set(orbiter._orbiter.camera.target, target);
vec3.set(orbiter.target, target);
}
if (position) {
vec3.set(orbiter._orbiter.camera.position, position);
vec3.set(entity.transform.position, position);
}
mat4.lookAt(
TEMP_MAT4,
orbiter._orbiter.camera.position,
orbiter._orbiter.camera.target,
orbiter._orbiter.camera.up,
);
mat4.invert(TEMP_MAT4);
quat.fromMat4(entity.transform.rotation, TEMP_MAT4);
quat.set(
orbiter._orbiter.camera.rotationCache,
entity.transform.rotation,
);
orbiter.lat = orbiter._orbiter.lat;
orbiter.lon = orbiter._orbiter.lon;
orbiter.distance = orbiter._orbiter.distance;
// TODO: need to check lat/lon/dist change?
entity.transform.dirty = true;
camera.dirty = true;
},
};
orbiter._orbiter = createOrbiter({
element: orbiter.element || document.body, //TODO: element used to default to ctx.gl.canvas
autoUpdate: false,
camera: proxyCamera,
position: proxyCamera.position,
maxDistance: camera.far * 0.9,
});
orbiter._orbiter.updateCamera();
orbiter.distance = orbiter._orbiter.distance;
orbiter.lat = orbiter._orbiter.lat;
orbiter.lon = orbiter._orbiter.lon;
orbiter._orbiter.distanceCache = orbiter._orbiter.distance;
orbiter._orbiter.latCache = orbiter._orbiter.lat;
orbiter._orbiter.lonCache = orbiter._orbiter.lon;
} else {
if (camera.dirty) {
camera.dirty = false;
updateCameraProjection(camera, entity._transform);
}
let newPosition = null;
let newTarget = null;
// check if camera moved without _orbiter intervention
if (
vec3.distance(
orbiter._orbiter.camera.position,
entity.transform.position,
) > utils.EPSILON
) {
newPosition = [...entity.transform.position];
}
//check if camera rotated without orbiter intervention
if (
vec3.distance(
orbiter._orbiter.camera.rotationCache,
entity.transform.rotation,
) > utils.EPSILON
) {
// console.log("sync with camera rotation");
newTarget = [0, 0, -orbiter._orbiter.distance];
const useInvMatrix = false;
if (useInvMatrix) {
vec3.multMat4(newTarget, camera.invViewMatrix); //this is out of date?
} else {
vec3.multQuat(newTarget, entity.transform.rotation);
vec3.add(newTarget, entity.transform.position);
}
}
// check if camera orbiter moved without _orbiter intervention
if (
vec3.distance(orbiter.target, orbiter._orbiter.camera.target) >
utils.EPSILON
) {
newTarget = orbiter.target;
// console.log("sync with orbiter target");
}
if (newPosition || newTarget) {
const opts = {};
if (newPosition) {
opts.position = [...newPosition];
}
if (newTarget) {
opts.target = [...newTarget];
}
orbiter._orbiter.camera.set(opts);
orbiter._orbiter.set({
camera: orbiter._orbiter.camera,
});
} else {
// added cached properties to know if distance,lon,lat changed externally
// and if they haven't give a chance to _orbiter.updateCamera() to update them
// comparing orbiter.distance to orbiter._orbiter.distance would always overwrite orbiter
if (orbiter.distance !== orbiter._orbiter.distanceCache) {
orbiter._orbiter.distanceCache = orbiter.distance;
orbiter._orbiter.set({ distance: orbiter.distance });
}
if (orbiter.lon !== orbiter._orbiter.lonCache) {
orbiter._orbiter.lonCache = orbiter.lon;
orbiter._orbiter.set({ lon: orbiter.lon });
}
if (orbiter.lat !== orbiter._orbiter.latCache) {
orbiter._orbiter.latCache = orbiter.lat;
orbiter._orbiter.set({ lat: orbiter.lat });
}
}
orbiter._orbiter.updateCamera();
mat4.identity(camera.invViewMatrix);
mat4.translate(camera.invViewMatrix, entity.transform.position);
mat4.mult(
camera.invViewMatrix,
mat4.fromQuat(TEMP_MAT4, entity.transform.rotation),
);
mat4.set(camera.viewMatrix, camera.invViewMatrix);
mat4.invert(camera.viewMatrix);
}
} else {
// Camera manually updated or animation
if (entity.camera.dirty) {
entity.camera.dirty = false;
updateCameraProjection(entity.camera, entity._transform);
}
}
},
update(entities) {
for (let i = 0; i < entities.length; i++) {
const entity = entities[i];
if (entity.camera) {
if (!this.checkCamera(null, entity)) continue;
this.updateCameraEntity(entity);
}
}
},
});