@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
307 lines (234 loc) • 10.5 kB
JavaScript
import { assert } from "../../../../core/assert.js";
import { AABB3 } from "../../../../core/geom/3d/aabb/AABB3.js";
import { ResourceAccessKind } from "../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../core/model/ResourceAccessSpecification.js";
import { AbstractContextSystem } from "../../../ecs/system/AbstractContextSystem.js";
import { Transform } from '../../../ecs/transform/Transform.js';
import { ForwardPlusRenderingPlugin } from "../../render/forward_plus/plugin/ForwardPlusRenderingPlugin.js";
import { Camera } from '../camera/Camera.js';
import FrustumProjector from '../camera/FrustumProjector.js';
import { Light } from './Light.js';
import { LightContext } from "./LightContext.js";
import { LightType } from "./LightType.js";
import { setShadowCameraDimensionsDiscrete } from "./shadow/setShadowCameraDimensionsDiscrete.js";
import { ShadowManager } from "./shadow/ShadowManager.js";
import { ThreeLightCache } from "./three/ThreeLightCache.js";
class LightSystem extends AbstractContextSystem {
/**
*
* @param {Engine} engine
* @param settings
* @constructor
*/
constructor(engine, settings = {}) {
super(LightContext);
/**
*
* @type {GraphicsEngine}
*/
const graphics = engine.graphics;
assert.defined(graphics, 'graphics');
assert.defined(settings, 'settings');
this.dependencies = [Light, Transform];
this.components_used = [
ResourceAccessSpecification.from(Light, ResourceAccessKind.Write)
];
this.engine = engine;
this.scene = graphics.scene;
this.settings = settings;
/**
*
* @type {GraphicsEngine}
* @private
*/
this.__graphics = graphics;
/**
*
* @type {Camera}
* @private
*/
this.__camera_object = null;
this.bindings = [];
/**
*
* @type {ShadowManager}
* @private
*/
this.__shadows = new ShadowManager();
this.__shadows.graphics = graphics;
/**
*
* @type {boolean}
* @private
*/
this.__use_forward_plus = true;
/**
*
* @type {Reference<ForwardPlusRenderingPlugin>|null}
* @private
*/
this.__plugin = null;
/**
*
* @type {ThreeLightCache}
* @private
*/
this.__three_light_cache = new ThreeLightCache();
// connect light cache
this.__three_light_cache.attach(this.scene);
if (settings.preAllocateLightsDirection !== undefined) {
this.__three_light_cache.reserve(LightType.DIRECTION, settings.preAllocateLightsDirection);
}
if (settings.preAllocateLightsSpot !== undefined) {
this.__three_light_cache.reserve(LightType.SPOT, settings.preAllocateLightsSpot);
}
if (settings.preAllocateLightsPoint !== undefined) {
this.__three_light_cache.reserve(LightType.POINT, settings.preAllocateLightsPoint);
}
if (settings.preAllocateLightsAmbient !== undefined) {
this.__three_light_cache.reserve(LightType.AMBIENT, settings.preAllocateLightsAmbient);
}
}
get componentClass() {
return Light;
}
/**
*
* @param {string} name
* @param {string|number|boolean} value
*/
setConfiguration(name, value) {
this.settings[name] = value;
const em = this.entityManager;
if (em === null) {
return;
}
const dataset = em.dataset;
if (dataset === null) {
return;
}
dataset.traverseComponents(Light, this.applySettingsOne, this);
}
/**
*
* @param {Light} component
* @param {number} entity
*/
applySettingsOne(component, entity) {
const ctx = this.__getEntityContext(entity);
ctx.applySettings();
}
async startup(entityManager) {
if (this.__use_forward_plus) {
this.__plugin = await this.engine.plugins.acquire(ForwardPlusRenderingPlugin);
}
this.__graphics.on.visibilityConstructionEnded.add(this.__updateShadowCameraForActiveCamera, this);
}
async shutdown(entityManager) {
if (this.__plugin !== null) {
this.__plugin.release();
this.__plugin = null;
}
this.__graphics.on.visibilityConstructionEnded.remove(this.__updateShadowCameraForActiveCamera, this);
}
__updateShadowCameraForActiveCamera() {
const em = this.entityManager;
const dataset = em.dataset;
if (dataset !== null) {
dataset.traverseComponents(Camera, this.__updateShadowCameraForCamera, this);
}
}
/**
*
* @param {Light} light
* @param {number} lightEntity
* @private
*/
__visit_entity_to_update_shadow(light, lightEntity) {
/**
*
1) Calculate the 8 corners of the view frustum in world space. This can be done by using the inverse view-projection matrix to transform the 8 corners of the NDC cube (which in OpenGL is [‒1, 1] along each axis).
2) Transform the frustum corners to a space aligned with the shadow map axes. This would commonly be the directional light object's local space.
(In fact, steps 1 and 2 can be done in one step by combining the inverse view-projection matrix of the camera with the inverse world matrix of the light.)
3) Calculate the bounding box of the transformed frustum corners. This will be the view frustum for the shadow map.
4) Pass the bounding box's extents to glOrtho or similar to set up the orthographic projection matrix for the shadow map.
There are a couple caveats with this basic approach. First, the Z bounds for the shadow map will be tightly fit around the view frustum, which means that objects outside the view frustum, but between the view frustum and the light, may fall outside the shadow frustum. This could lead to missing shadows. To fix this, depth clamping can be enabled so that objects in front of the shadow frustum will be rendered with clamped Z instead of clipped. Alternatively, the Z-near of the shadow frustum can be pushed out to ensure any possible shadowers are included.
The bigger issue is that this produces a shadow frustum that continuously changes size and position as the camera moves around. This leads to shadows "swimming", which is a very distracting artifact. In order to fix this, it's common to do the following additional two steps:
1) Fix the overall size of the frustum based on the longest diagonal of the camera frustum. This ensures that the camera frustum can fit into the shadow frustum in any orientation. Don't allow the shadow frustum to change size as the camera rotates.
2) Discretize the position of the frustum, based on the size of texels in the shadow map. In other words, if the shadow map is 1024×1024, then you only allow the frustum to move around in discrete steps of 1/1024th of the frustum size. (You also need to increase the size of the frustum by a factor of 1024/1023, to give room for the shadow frustum and view frustum to slip against each other.)
If you do these, the shadow will remain rock solid in world space as the camera moves around. (It won't remain solid if the camera's FOV, near or far planes are changed, though.)
As a bonus, if you do all the above, you're well on your way to implementing cascaded shadow maps, which are "just" a set of shadow maps calculated from the view frustum as above, but using different view frustum near and far plane values to place each shadow map.
*/
const l = light.__threeObject;
if (l !== null && light.type.getValue() !== LightType.AMBIENT) {
//only non-ambient lights can cast shadow
if (light.castShadow.getValue()) {
l.castShadow = true;
three_update_shadow_camera_extents(this.__camera_object, l, this.__graphics);
} else {
l.castShadow = false;
}
}
}
/**
*
* @param {Camera} camera
* @param {number} cameraEntity
* @returns {boolean}
* @private
*/
__updateShadowCameraForCamera(camera, cameraEntity) {
const em = this.entityManager;
const dataset = em.dataset;
if (!camera.active.getValue()) {
return true;
}
/**
*
* @type {Camera}
* @private
*/
this.__camera_object = camera.object;
if (this.__camera_object === null) {
return false;
}
dataset.traverseComponents(Light, this.__visit_entity_to_update_shadow, this);
//stop further traversal
return false;
}
}
const scratch_aabb3 = new AABB3();
/**
* @author alteredq 28.1.2012 (three.js)
* @author Alex Goldring 02.06.2016 (Komrade)
* @param {Camera} camera
* @param {Light} light
*/
export function three_update_shadow_camera_extents(camera, light) {
// Fit shadow camera's ortho frustum to camera frustum
const shadow = light.shadow;
if (shadow === undefined) {
console.error(`Light does not have a shadow`, light);
return;
}
const shadowCamera = shadow.camera;
FrustumProjector.project(-1, 1, camera, shadowCamera.matrixWorldInverse, scratch_aabb3);
// fit shadow camera dimensions to span only what's visible in the active camera view
setShadowCameraDimensionsDiscrete(
shadow.mapSize,
shadowCamera,
scratch_aabb3.x0,
scratch_aabb3.y0,
scratch_aabb3.z0,
scratch_aabb3.x1,
scratch_aabb3.y1,
scratch_aabb3.z1
);
// collect all visible objects
shadowCamera.near = 0.1;
shadowCamera.far = 100;
shadowCamera.updateProjectionMatrix();
// small bias to prevent shadow acne
shadow.bias = -0.0001;
}
export default LightSystem;