UNPKG

gpu-curtains

Version:

gpu-curtains is a 3D WebGPU rendering engine. It can be used as a standalone 3D engine, but also includes extra classes focused on mapping 3d objects to DOM elements; It allows users to synchronize values such as position, sizing, or scale between them.

267 lines (266 loc) 8.95 kB
import { Quat } from "../../math/Quat.mjs"; import { Vec3 } from "../../math/Vec3.mjs"; import { Mat4 } from "../../math/Mat4.mjs"; import { Light } from "./Light.mjs"; import { SpotShadow } from "../shadows/SpotShadow.mjs"; //#region src/core/lights/SpotLight.ts const tempVec3 = new Vec3(); const tempQuat = new Quat(); const tempMat4 = new Mat4(); /** * Create a spot light, that is emitted from a single point in one direction, along a cone that increases in size the further from the light it gets. * * This light can cast {@link SpotShadow}. * * @example * ```javascript * // assuming 'renderer' is a valid Camera renderer * * // this spot light will not cast any shadows * const spotLight = new SpotLight(renderer, { * color: new Vec3(1), * intensity: 1, * position: new Vec3(5, 2, 3), * penumbra: 0.5, * }) * * // this spot light will cast shadows * const spotLightWithShadows = new SpotLight(renderer, { * color: new Vec3(1), * intensity: 1, * position: new Vec3(-10, 10, -5), * target: new Vec3(0, 0.5, 0), * shadow: { * intensity: 1, * }, * }) * * // this spot light will ALSO cast shadows! * const anotherSpotLightWithShadows = new SpotLight(renderer, { * color: new Vec3(1), * intensity: 2, * position: new Vec3(12, 0.5, 5), * target: new Vec3(3), * shadow: {}, // that's enough to start casting shadows * }) * * // this spot light will cast shadows as well... * const lastSpotLightWithShadows = new SpotLight(renderer, { * color: new Vec3(1), * intensity: 1, * position: new Vec3(10), * }) * * // ... because we're telling it here to start casting shadows * lastSpotLightWithShadows.shadow.cast() * ``` */ var SpotLight = class extends Light { /** * The {@link Vec3 | direction} of the {@link SpotLight} is the {@link target} minus the actual {@link position}. * @private */ #direction; /** @ignore */ #angle; /** @ignore */ #penumbra; /** @ignore */ #range; /** * SpotLight constructor * @param renderer - {@link CameraRenderer} or {@link GPUCurtains} used to create this {@link SpotLight}. * @param parameters - {@link SpotLightBaseParams} used to create this {@link SpotLight}. */ constructor(renderer, { label = "SpotLight", color = new Vec3(1), intensity = 1, position = new Vec3(1), target = new Vec3(), angle = Math.PI / 3, penumbra = 0, range = 0, shadow = null } = {}) { super(renderer, { label, color, intensity, type: "spotLights" }); this.options = { ...this.options, position, range, angle, penumbra, target, shadow }; this.#direction = new Vec3(); this.position.copy(position); this.target = new Vec3(); this.target.onChange(() => { this.lookAt(this.target); }); this.target.copy(target); this.position.onChange(() => { this.lookAt(this.target); }); if (this.target.lengthSq() === 0) this.lookAt(this.target); this.angle = angle; this.penumbra = penumbra; this.range = range; this.parent = this.renderer.scene; this.shadow = new SpotShadow(this.renderer, { autoRender: false, light: this }); if (shadow) this.shadow.cast(shadow); } /** * Set or reset this {@link SpotLight} {@link CameraRenderer}. * @param renderer - New {@link CameraRenderer} or {@link GPUCurtains} instance to use. */ setRenderer(renderer) { super.setRenderer(renderer); if (this.shadow) this.shadow.setRenderer(renderer); } /** * Resend all properties to the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}. Called when the maximum number of {@link SpotLight} has been overflowed or when updating the {@link SpotLight} {@link renderer}. * @param resetShadow - Whether to reset the {@link SpotLight} shadow if any. Set to `true` when the {@link renderer} number of {@link SpotLight} has been overflown, `false` when the {@link renderer} has been changed (since the shadow will reset itself). */ reset(resetShadow = true) { super.reset(); this.onPropertyChanged("range", this.range); this.onPropertyChanged("coneCos", Math.cos(this.angle)); this.onPropertyChanged("penumbraCos", Math.cos(this.angle * (1 - this.penumbra))); this.onPropertyChanged("position", this.actualPosition); this.onPropertyChanged("direction", this.#direction); if (this.shadow && resetShadow) this.shadow.reset(); } /** * Get this {@link SpotLight} intensity. * @returns - The {@link SpotLight} intensity. */ get intensity() { return super.intensity; } /** * Set this {@link SpotLight} intensity and clear shadow if intensity is `0`. * @param value - The new {@link SpotLight} intensity. */ set intensity(value) { super.intensity = value; if (this.shadow && this.shadow.isActive && !value) this.shadow.clearDepthTexture(); } /** * Set the {@link SpotLight} position and direction based on the {@link target} and the {@link worldMatrix} translation. */ setPositionDirection() { this.onPropertyChanged("position", this.actualPosition); this.#direction.copy(this.target).sub(this.actualPosition).normalize(); this.onPropertyChanged("direction", this.#direction); if (this.shadow) this.shadow.setPosition(); } /** * Update the {@link target} and therefore direction directly from the {@link worldMatrix}, in case a transformation (especially rotation) has been applied to a parent of this {@link SpotLight} instead of updating the {@link target} directly. */ updateTargetFromWorldMatrix() { tempVec3.set(1); this.worldMatrix.getScale(tempVec3); tempMat4.identity(); for (const col of [ 0, 1, 2 ]) { tempMat4.elements[col] = this.worldMatrix.elements[col] / tempVec3.x; tempMat4.elements[col + 4] = this.worldMatrix.elements[col + 4] / tempVec3.y; tempMat4.elements[col + 8] = this.worldMatrix.elements[col + 8] / tempVec3.z; } tempMat4.getRotation(tempQuat); tempQuat.normalize(); tempVec3.set(0, 0, -1); tempVec3.applyQuat(tempQuat); tempVec3.add(this.actualPosition); this.target._x = tempVec3.x; this.target._y = tempVec3.y; this.target._z = tempVec3.z; this.setPositionDirection(); } /** * Get this {@link SpotLight} angle. * @returns - The {@link SpotLight} angle. */ get angle() { return this.#angle; } /** * Set this {@link SpotLight} angle and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}. * @param value - The new {@link SpotLight} angle in the `[0, PI / 2]` range. */ set angle(value) { this.#angle = Math.min(Math.PI / 2, Math.max(0, value)); this.onPropertyChanged("coneCos", Math.cos(this.angle)); this.onPropertyChanged("penumbraCos", Math.cos(this.angle * (1 - this.penumbra))); this.shadow?.setCameraFov(); } /** * Get this {@link SpotLight} penumbra. * @returns - The {@link SpotLight} penumbra. */ get penumbra() { return this.#penumbra; } /** * Set this {@link SpotLight} penumbra and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}. * @param value - The new {@link SpotLight} penumbra in the `[0, 1]` range. */ set penumbra(value) { this.#penumbra = Math.min(1, Math.max(0, value)); this.onPropertyChanged("penumbraCos", Math.cos(this.angle * (1 - this.penumbra))); } /** * Get this {@link SpotLight} range. * @returns - The {@link SpotLight} range. */ get range() { return this.#range; } /** * Set this {@link SpotLight} range and update the {@link CameraRenderer} corresponding {@link core/bindings/BufferBinding.BufferBinding | BufferBinding}. * @param value - The new {@link SpotLight} range. */ set range(value) { this.#range = Math.max(0, value); this.onPropertyChanged("range", this.range); if (this.shadow) this.shadow.camera.far = this.range ? this.range : this.shadow.options.camera.far; } /** * Rotate this {@link SpotLight} so it looks at the {@link Vec3 | target}. * @param target - {@link Vec3} to look at. Default to `new Vec3()`. */ lookAt(target = new Vec3()) { this.updateModelMatrix(); this.updateWorldMatrix(true, false); if (this.actualPosition.x === 0 && this.actualPosition.y !== 0 && this.actualPosition.z === 0) this.up.set(0, 0, 1); else this.up.set(0, 1, 0); this.applyLookAt(this.actualPosition, target); } /** * If the {@link modelMatrix | model matrix} has been updated, set the new position and direction from the {@link worldMatrix} translation. */ updateMatrixStack() { super.updateMatrixStack(); if (this.matricesNeedUpdate) this.setPositionDirection(); } /** * Tell the {@link renderer} that the maximum number of {@link SpotLight} has been overflown. * @param lightsType - {@link type} of this light. */ onMaxLightOverflow(lightsType) { super.onMaxLightOverflow(lightsType); this.shadow?.setRendererBinding(); } /** * Destroy this {@link SpotLight} and associated {@link SpotShadow}. */ destroy() { super.destroy(); this.shadow?.destroy(); } }; //#endregion export { SpotLight };