Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSM Support #273

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/engine/camera/camera.game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export function createRemotePerspectiveCamera(ctx: GameState, props?: Perspectiv
const cameraBufferView = createObjectBufferView(perspectiveCameraSchema, ArrayBuffer);

cameraBufferView.layers[0] = props?.layers === undefined ? 1 : props.layers;
cameraBufferView.zfar[0] = props?.zfar || 2000;
cameraBufferView.zfar[0] = props?.zfar || 500;
cameraBufferView.znear[0] = props?.znear === undefined ? 0.1 : props.znear;
cameraBufferView.aspectRatio[0] = props?.aspectRatio || 0; // 0 for automatic aspect ratio defined by canvas
cameraBufferView.yfov[0] = props?.yfov === undefined ? glMatrix.toRadian(50) : props.yfov;
Expand Down
5 changes: 5 additions & 0 deletions src/engine/camera/camera.render.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { OrthographicCamera, PerspectiveCamera, Scene, MathUtils } from "three";

import { getReadObjectBufferView, ReadObjectTripleBufferView } from "../allocator/ObjectBufferView";
import { getModule } from "../module/module.common";
import { RendererNodeTripleBuffer } from "../node/node.common";
import { LocalNode, updateTransformFromNode } from "../node/node.render";
import { RendererModule } from "../renderer/renderer.render";
import { RenderThreadState } from "../renderer/renderer.render";
import { ResourceId } from "../resource/resource.common";
import { getLocalResource } from "../resource/resource.render";
Expand Down Expand Up @@ -58,6 +60,7 @@ export function updateNodeCamera(
node: LocalNode,
nodeReadView: ReadObjectTripleBufferView<RendererNodeTripleBuffer>
) {
const rendererModule = getModule(ctx, RendererModule);
const currentCameraResourceId = node.camera?.resourceId || 0;
const nextCameraResourceId = nodeReadView.camera[0];

Expand Down Expand Up @@ -101,6 +104,8 @@ export function updateNodeCamera(
// Renderer will update aspect based on the viewport if the aspectRatio is set to 0
if (cameraView.aspectRatio[0]) {
perspectiveCamera.aspect = cameraView.aspectRatio[0];
} else {
perspectiveCamera.aspect = rendererModule.canvasWidth / rendererModule.canvasHeight;
}

if (cameraView.projectionMatrixNeedsUpdate[0]) {
Expand Down
242 changes: 242 additions & 0 deletions src/engine/light/CSMDirectionalLight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { vec3 } from "gl-matrix";
import {
Box3,
DirectionalLight,
Material,
MathUtils,
Matrix4,
Object3D,
PerspectiveCamera,
Vector2,
Vector3,
} from "three";

import CSMFrustum from "./CSMFrustum";
import { CSMHelper } from "./CSMHelper";

const DEBUG = false;

export const NUM_CSM_CASCADES = 1;
const SHADOW_NEAR = 1;
const SHADOW_FAR = 200;
export const MAX_SHADOW_DISTANCE = 100;
const SHADOW_MAP_SIZE = 2048;
const SHADOW_BIAS = 0;
const LIGHT_MARGIN = 50;

const logarithmicSplits: number[] = [];
const uniformSplits: number[] = [];
const cameraToLightMatrix = new Matrix4();
const lightSpaceFrustum = new CSMFrustum();
const center = new Vector3();
const bbox = new Box3();

export class CSMDirectionalLight extends Object3D {
private color: vec3 = [1, 1, 1];
private intensity = 1;
private _castShadow = false;
private camera?: PerspectiveCamera;
private splits: number[] = [];
private helper?: CSMHelper;

public readonly lights: DirectionalLight[] = [];
public readonly mainFrustum: CSMFrustum;
public readonly frustums: CSMFrustum[] = [];

public direction: Vector3 = new Vector3(1, -1, 1).normalize();

public isCSMDirectionalLight = true;

constructor(color?: vec3, intensity?: number) {
super();

if (color) {
this.color = color;
}

if (intensity !== undefined) {
this.intensity = intensity;
}

this.mainFrustum = new CSMFrustum();

for (let i = 0; i < NUM_CSM_CASCADES; i++) {
const light = new DirectionalLight();
light.color.fromArray(this.color);
light.intensity = this.intensity;
light.castShadow = this._castShadow;
light.shadow.mapSize.setScalar(SHADOW_MAP_SIZE);
light.shadow.camera.near = SHADOW_NEAR;
light.shadow.camera.far = SHADOW_FAR;
this.add(light);
this.add(light.target);
this.lights.push(light);
}

if (DEBUG) {
const helper = new CSMHelper();
this.helper = helper;
this.add(helper);
}
}

public getColor(): vec3 {
return this.color;
}

public setColor(color: vec3) {
vec3.copy(this.color, color);

for (let i = 0; i < this.lights.length; i++) {
this.lights[i].color.fromArray(color);
}
}

public getIntensity(): number {
return this.intensity;
}

public setIntensity(intensity: number) {
this.intensity = intensity;

for (let i = 0; i < this.lights.length; i++) {
this.lights[i].intensity = intensity;
}
}

public getCastShadow(): boolean {
return this._castShadow;
}

public setCastShadow(castShadow: boolean) {
this._castShadow = true;

for (let i = 0; i < this.lights.length; i++) {
this.lights[i].castShadow = castShadow;
}
}

private updateFrustums(camera: PerspectiveCamera) {
const far = Math.min(camera.far, MAX_SHADOW_DISTANCE);

logarithmicSplits.length = 0;
uniformSplits.length = 0;
this.splits.length = 0;

for (let i = 1; i < NUM_CSM_CASCADES; i++) {
logarithmicSplits.push((camera.near * (far / camera.near) ** (i / NUM_CSM_CASCADES)) / far);
uniformSplits.push((camera.near + ((far - camera.near) * i) / NUM_CSM_CASCADES) / far);
}

logarithmicSplits.push(1);
uniformSplits.push(1);

for (let i = 1; i < NUM_CSM_CASCADES; i++) {
this.splits.push(MathUtils.lerp(uniformSplits[i - 1], logarithmicSplits[i - 1], 0.5));
}

this.splits.push(1);

camera.updateProjectionMatrix();

this.mainFrustum.setFromProjectionMatrix(camera.projectionMatrix, MAX_SHADOW_DISTANCE);
this.mainFrustum.split(this.splits, this.frustums);

for (let i = 0; i < this.frustums.length; i++) {
const light = this.lights[i];
const shadowCamera = light.shadow.camera;
const frustum = this.frustums[i];

// Get the two points that represent that furthest points on the frustum assuming
// that's either the diagonal across the far plane or the diagonal across the whole
// frustum itself.
const nearVerts = frustum.vertices.near;
const farVerts = frustum.vertices.far;
const point1 = farVerts[0];
let point2;
if (point1.distanceTo(farVerts[2]) > point1.distanceTo(nearVerts[2])) {
point2 = farVerts[2];
} else {
point2 = nearVerts[2];
}

let squaredBBWidth = point1.distanceTo(point2);

// expand the shadow extents by the fade margin
// TODO: shouldn't this be Math.min?
const far = Math.max(camera.far, MAX_SHADOW_DISTANCE);
const linearDepth = frustum.vertices.far[0].z / (far - camera.near);
const margin = 0.25 * Math.pow(linearDepth, 2.0) * (far - camera.near);

squaredBBWidth += margin;

shadowCamera.left = -squaredBBWidth / 2;
shadowCamera.right = squaredBBWidth / 2;
shadowCamera.top = squaredBBWidth / 2;
shadowCamera.bottom = -squaredBBWidth / 2;
shadowCamera.updateProjectionMatrix();

light.shadow.bias = SHADOW_BIAS * squaredBBWidth;
}
}

public updateMaterial(camera: PerspectiveCamera, material: Material) {
for (let i = 0; i < NUM_CSM_CASCADES; i++) {
const splitRange = material.userData.csmSplits.value[i] as Vector2;
const amount = this.splits[i];
const prev = this.splits[i - 1] || 0;
splitRange.x = prev;
splitRange.y = amount;
}

const far = Math.min(camera.far, MAX_SHADOW_DISTANCE);

material.userData.cameraNear.value = camera.near;
material.userData.shadowFar.value = far;
}

public update(camera: PerspectiveCamera) {
if (camera !== this.camera) {
this.camera = camera;
this.updateFrustums(camera);
}

const frustums = this.frustums;

for (let i = 0; i < frustums.length; i++) {
const light = this.lights[i];
const shadowCam = light.shadow.camera;
const texelWidth = (shadowCam.right - shadowCam.left) / SHADOW_MAP_SIZE;
const texelHeight = (shadowCam.top - shadowCam.bottom) / SHADOW_MAP_SIZE;
light.shadow.camera.updateMatrixWorld(true);
cameraToLightMatrix.multiplyMatrices(light.shadow.camera.matrixWorldInverse, camera.matrixWorld);
frustums[i].toSpace(cameraToLightMatrix, lightSpaceFrustum);

const nearVerts = lightSpaceFrustum.vertices.near;
const farVerts = lightSpaceFrustum.vertices.far;
bbox.makeEmpty();

for (let j = 0; j < 4; j++) {
bbox.expandByPoint(nearVerts[j]);
bbox.expandByPoint(farVerts[j]);
}

bbox.getCenter(center);
center.z = bbox.max.z + LIGHT_MARGIN;
center.x = Math.floor(center.x / texelWidth) * texelWidth;
center.y = Math.floor(center.y / texelHeight) * texelHeight;
center.applyMatrix4(light.shadow.camera.matrixWorld);

light.position.copy(center);
light.target.position.copy(center);

light.target.position.x += this.direction.x;
light.target.position.y += this.direction.y;
light.target.position.z += this.direction.z;
}

if (this.helper) {
this.helper.update(this, camera);
}
}
}
103 changes: 103 additions & 0 deletions src/engine/light/CSMFrustum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Vector3, Matrix4 } from "three";

const inverseProjectionMatrix = new Matrix4();

interface CSMFrustumData {
projectionMatrix?: Matrix4;
maxFar?: number;
}

export default class CSMFrustum {
vertices: {
near: Vector3[];
far: Vector3[];
};

constructor(data?: CSMFrustumData) {
data = data || {};

this.vertices = {
near: [new Vector3(), new Vector3(), new Vector3(), new Vector3()],
far: [new Vector3(), new Vector3(), new Vector3(), new Vector3()],
};

if (data.projectionMatrix !== undefined) {
this.setFromProjectionMatrix(data.projectionMatrix, data.maxFar || 10000);
}
}

setFromProjectionMatrix(projectionMatrix: Matrix4, maxFar: number) {
const isOrthographic = projectionMatrix.elements[2 * 4 + 3] === 0;

inverseProjectionMatrix.copy(projectionMatrix).invert();

// 3 --- 0 vertices.near/far order
// | |
// 2 --- 1
// clip space spans from [-1, 1]

this.vertices.near[0].set(1, 1, -1);
this.vertices.near[1].set(1, -1, -1);
this.vertices.near[2].set(-1, -1, -1);
this.vertices.near[3].set(-1, 1, -1);
this.vertices.near.forEach(function (v) {
v.applyMatrix4(inverseProjectionMatrix);
});

this.vertices.far[0].set(1, 1, 1);
this.vertices.far[1].set(1, -1, 1);
this.vertices.far[2].set(-1, -1, 1);
this.vertices.far[3].set(-1, 1, 1);
this.vertices.far.forEach(function (v) {
v.applyMatrix4(inverseProjectionMatrix);

const absZ = Math.abs(v.z);
if (isOrthographic) {
v.z *= Math.min(maxFar / absZ, 1.0);
} else {
v.multiplyScalar(Math.min(maxFar / absZ, 1.0));
}
});

return this.vertices;
}

split(breaks: number[], target: CSMFrustum[]) {
while (breaks.length > target.length) {
target.push(new CSMFrustum());
}
target.length = breaks.length;

for (let i = 0; i < breaks.length; i++) {
const cascade = target[i];

if (i === 0) {
for (let j = 0; j < 4; j++) {
cascade.vertices.near[j].copy(this.vertices.near[j]);
}
} else {
for (let j = 0; j < 4; j++) {
cascade.vertices.near[j].lerpVectors(this.vertices.near[j], this.vertices.far[j], breaks[i - 1]);
}
}

if (i === breaks.length - 1) {
for (let j = 0; j < 4; j++) {
cascade.vertices.far[j].copy(this.vertices.far[j]);
}
} else {
for (let j = 0; j < 4; j++) {
cascade.vertices.far[j].lerpVectors(this.vertices.near[j], this.vertices.far[j], breaks[i]);
}
}
}
}

toSpace(cameraMatrix: Matrix4, target: CSMFrustum) {
for (let i = 0; i < 4; i++) {
target.vertices.near[i].copy(this.vertices.near[i]).applyMatrix4(cameraMatrix);

target.vertices.far[i].copy(this.vertices.far[i]).applyMatrix4(cameraMatrix);
}
}
}
Loading