85 lines
3.5 KiB
TypeScript
85 lines
3.5 KiB
TypeScript
import * as THREE from 'three';
|
|
|
|
export class CircularFrustum {
|
|
public readonly apex = new THREE.Vector3(); // Cone apex (camera position)
|
|
public readonly axisNormalized = new THREE.Vector3(); // normalized (unit) axis direction (camera → screen point)
|
|
|
|
public halfAngle: number = 0; // Half-angle of the cone in radians
|
|
public cosHalfAngle: number = 0; // cos(halfAngle) — cached
|
|
public sinHalfAngle: number = 0; // sin(halfAngle) — cached
|
|
|
|
/**
|
|
* Build a CircularFrustum from a screen-space point and pixel threshold.
|
|
*
|
|
* @param screenPointNormalized point in normalised device coordinates (NDC) [-1, 1]
|
|
* @param camera PerspectiveCamera or OrthographicCamera
|
|
* @param thresholdPx Screen-space radius in pixels
|
|
* @param screenSize renderer viewport size in pixels (e.g. from viewport.getBoundingClientRect())
|
|
*/
|
|
public setFromScreenPoint(
|
|
screenPointNormalized: THREE.Vector2Like, // in [-1, 1] space
|
|
screenSize: THREE.Vector2Like,
|
|
camera: THREE.PerspectiveCamera | THREE.OrthographicCamera,
|
|
thresholdPx: number, // in screen pixels
|
|
): void {
|
|
// cone apex
|
|
camera.getWorldPosition(this.apex);
|
|
|
|
// cone central ray (axis)
|
|
const centerNDC = new THREE.Vector3(screenPointNormalized.x, screenPointNormalized.y, 0.5); // center
|
|
const centerWorld = centerNDC.clone().unproject(camera);
|
|
this.axisNormalized.copy(centerWorld.sub(this.apex)).normalize();
|
|
|
|
// frustum near circle bottommost point
|
|
const bottomNDC = centerNDC.clone().add({ x: 0, y: (thresholdPx / screenSize.y) * 2, z: 0 });
|
|
const bottomWorld = bottomNDC.clone().unproject(camera);
|
|
const downAxis = bottomWorld.sub(this.apex).normalize();
|
|
|
|
// cone half-angle
|
|
const halfAngle = Math.acos(THREE.MathUtils.clamp(this.axisNormalized.dot(downAxis), -1, 1));
|
|
this.set(this.apex, this.axisNormalized, halfAngle);
|
|
|
|
// console.log({
|
|
// screenPointNormalized,
|
|
// screenSize,
|
|
// thresholdPx,
|
|
// centerNDC: centerNDC.toArray(),
|
|
// centerWorld: centerWorld.toArray(),
|
|
// bottomNDC: bottomNDC.toArray(),
|
|
// bottomWorld: bottomWorld.toArray(),
|
|
// downAxis: downAxis.toArray(),
|
|
// apex: this.apex.toArray(),
|
|
// axisNormalized: this.axisNormalized,
|
|
// halfAngle,
|
|
// });
|
|
// console.log(this.apex.toArray());
|
|
}
|
|
public getCircleAtDepth(depth: number): { center: THREE.Vector3Like, radius: number } {
|
|
const center = this.apex.clone().addScaledVector(this.axisNormalized, depth);
|
|
const radius = Math.tan(this.halfAngle) * depth;
|
|
|
|
return {
|
|
center,
|
|
radius,
|
|
}
|
|
}
|
|
|
|
private set(apex: THREE.Vector3Like, axisNormalized: THREE.Vector3Like, halfAngle: number) {
|
|
this.apex.copy(apex);
|
|
this.axisNormalized.copy(axisNormalized);
|
|
this.halfAngle = halfAngle;
|
|
this.cosHalfAngle = Math.cos(this.halfAngle);
|
|
this.sinHalfAngle = Math.sin(this.halfAngle);
|
|
}
|
|
|
|
public transform(matrix: THREE.Matrix4): CircularFrustum {
|
|
const transformed = new CircularFrustum();
|
|
|
|
const localApex = this.apex.clone().applyMatrix4(matrix);
|
|
const localAxis = this.axisNormalized.clone().transformDirection(matrix).normalize();
|
|
transformed.set(localApex, localAxis, this.halfAngle);
|
|
|
|
return transformed;
|
|
}
|
|
}
|