210 lines
7.9 KiB
TypeScript
210 lines
7.9 KiB
TypeScript
import { SpotLight } from '@babylonjs/core/Lights/spotLight';
|
||
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
|
||
import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent';
|
||
import { Vector3, Quaternion } from '@babylonjs/core/Maths/math.vector';
|
||
import { Monobehiver } from '../base/Monobehiver';
|
||
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
|
||
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
|
||
import { Color3 } from '@babylonjs/core/Maths/math.color';
|
||
import { GizmoManager } from '@babylonjs/core/Gizmos/gizmoManager';
|
||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||
import { Mesh } from '@babylonjs/core/Meshes/mesh';
|
||
|
||
type DebugMarkers = {
|
||
marker: Mesh;
|
||
arrow: Mesh;
|
||
gizmoManager: GizmoManager;
|
||
onKey: (e: KeyboardEvent) => void;
|
||
};
|
||
|
||
/**
|
||
* 灯光管理类- 负责创建和管理场景灯光
|
||
*/
|
||
export class AppLight extends Monobehiver {
|
||
lightList: SpotLight[];
|
||
shadowGenerator: ShadowGenerator | null;
|
||
debugMarkers?: DebugMarkers;
|
||
coneMesh?: Mesh;
|
||
updateCone?: () => void;
|
||
|
||
constructor(mainApp: any) {
|
||
super(mainApp);
|
||
this.lightList = [];
|
||
this.shadowGenerator = null;
|
||
}
|
||
|
||
/** 初始化灯光并开启阴影 */
|
||
Awake(): void {
|
||
const light = new SpotLight(
|
||
"mainLight",
|
||
new Vector3(-0.6, 2.12, 2),
|
||
new Vector3(0, -0.5, -1),
|
||
Math.PI * 0.6, // angle 弧度
|
||
);
|
||
|
||
light.angle = 1.5;
|
||
light.innerAngle = 1;
|
||
light.exponent = 2;
|
||
light.diffuse = new Color3(1, 0.86, 0.80);
|
||
light.specular = new Color3(1, 1, 1);
|
||
light.intensity = 60;
|
||
light.shadowMinZ = 0.01;
|
||
light.shadowMaxZ = 100;
|
||
light.range = 5000;
|
||
|
||
const generator = new ShadowGenerator(4096, light);
|
||
generator.usePercentageCloserFiltering = true;
|
||
generator.filteringQuality = ShadowGenerator.QUALITY_HIGH;
|
||
generator.transparencyShadow = true;
|
||
|
||
this.lightList.push(light);
|
||
this.shadowGenerator = generator;
|
||
}
|
||
|
||
/** 将网格添加为阴影投射者 */
|
||
addShadowCaster(mesh: AbstractMesh): void {
|
||
if (this.shadowGenerator) {
|
||
this.shadowGenerator.addShadowCaster(mesh);
|
||
}
|
||
}
|
||
|
||
/** 设置主光源强度 */
|
||
setIntensity(intensity: number): void {
|
||
if (this.lightList[0]) this.lightList[0].intensity = intensity;
|
||
}
|
||
|
||
/** 创建灯光可视化调试器 - W键拖拽位置,E键旋转方向 */
|
||
enableLightDebug(): void {
|
||
const scene = this.mainApp.appScene.object;
|
||
const light = this.lightList[0];
|
||
if (!light || !scene) return;
|
||
|
||
const marker = MeshBuilder.CreateSphere("lightMarker", { diameter: 0.3 }, scene);
|
||
marker.position = light.position.clone();
|
||
const mat = new StandardMaterial("lightMat", scene);
|
||
mat.emissiveColor = Color3.Yellow();
|
||
marker.material = mat;
|
||
|
||
const arrow = MeshBuilder.CreateCylinder("lightArrow", { height: 1, diameterTop: 0, diameterBottom: 0.1 }, scene);
|
||
arrow.parent = marker;
|
||
arrow.position.set(0, 0, 0.6);
|
||
arrow.rotation.x = Math.PI / 2;
|
||
const arrowMat = new StandardMaterial("arrowMat", scene);
|
||
arrowMat.emissiveColor = Color3.Red();
|
||
arrow.material = arrowMat;
|
||
|
||
const dir = light.direction.normalize();
|
||
marker.rotation.y = Math.atan2(dir.x, dir.z);
|
||
marker.rotation.x = -Math.asin(dir.y);
|
||
|
||
const gizmoManager = new GizmoManager(scene);
|
||
gizmoManager.attachableMeshes = [marker];
|
||
gizmoManager.usePointerToAttachGizmos = false;
|
||
gizmoManager.attachToMesh(marker);
|
||
|
||
scene.onBeforeRenderObservable.add(() => {
|
||
light.position.copyFrom(marker.position);
|
||
const forward = new Vector3(0, 0, 1);
|
||
const rotationMatrix = marker.getWorldMatrix().getRotationMatrix();
|
||
light.direction = Vector3.TransformNormal(forward, rotationMatrix).normalize();
|
||
});
|
||
|
||
const onKey = (e: KeyboardEvent) => {
|
||
if (e.key === 'w' || e.key === 'W') {
|
||
gizmoManager.positionGizmoEnabled = true;
|
||
gizmoManager.rotationGizmoEnabled = false;
|
||
} else if (e.key === 'e' || e.key === 'E') {
|
||
gizmoManager.positionGizmoEnabled = false;
|
||
gizmoManager.rotationGizmoEnabled = true;
|
||
}
|
||
};
|
||
window.addEventListener('keydown', onKey);
|
||
gizmoManager.positionGizmoEnabled = true;
|
||
|
||
this.debugMarkers = { marker, arrow, gizmoManager, onKey };
|
||
}
|
||
|
||
/** 隐藏灯光调试器 */
|
||
disableLightDebug(): void {
|
||
if (this.debugMarkers) {
|
||
window.removeEventListener('keydown', this.debugMarkers.onKey);
|
||
this.debugMarkers.gizmoManager.dispose();
|
||
this.debugMarkers.arrow.dispose();
|
||
this.debugMarkers.marker.dispose();
|
||
this.debugMarkers = undefined;
|
||
}
|
||
}
|
||
|
||
/** 创建聚光灯可视化Gizmo - 带光锥范围 */
|
||
createLightGizmo(): void {
|
||
const scene = this.mainApp.appScene.object;
|
||
const light = this.lightList[0];
|
||
if (!light || !scene) return;
|
||
|
||
const coneLength = 3;
|
||
const updateCone = () => {
|
||
if (this.coneMesh) this.coneMesh.dispose();
|
||
const radius = Math.tan(light.angle) * coneLength;
|
||
const cone = MeshBuilder.CreateCylinder("lightCone", {
|
||
height: coneLength,
|
||
diameterTop: radius * 2,
|
||
diameterBottom: 0
|
||
}, scene);
|
||
const mat = new StandardMaterial("coneMat", scene);
|
||
mat.emissiveColor = Color3.Yellow();
|
||
mat.alpha = 0.2;
|
||
mat.wireframe = true;
|
||
cone.material = mat;
|
||
|
||
cone.position = light.position.add(light.direction.scale(coneLength / 2));
|
||
const up = new Vector3(0, 1, 0);
|
||
const axis = Vector3.Cross(up, light.direction).normalize();
|
||
const angle = Math.acos(Vector3.Dot(up, light.direction.normalize()));
|
||
if (axis.length() > 0.001) cone.rotationQuaternion = Quaternion.RotationAxis(axis, angle);
|
||
|
||
this.coneMesh = cone;
|
||
};
|
||
|
||
updateCone();
|
||
this.updateCone = updateCone;
|
||
}
|
||
|
||
/** 创建angle和innerAngle调试滑动条 */
|
||
createAngleSliders(): void {
|
||
const light = this.lightList[0];
|
||
if (!light) return;
|
||
|
||
const container = document.createElement('div');
|
||
container.style.cssText = 'position:fixed;top:10px;right:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:5px;color:#fff;font-size:12px;z-index:1000';
|
||
|
||
const createSlider = (label: string, value: number, min: number, max: number, onChange: (v: number) => void) => {
|
||
const wrap = document.createElement('div');
|
||
wrap.style.marginBottom = '8px';
|
||
const lbl = document.createElement('div');
|
||
lbl.textContent = `${label}: ${value}`;
|
||
const slider = document.createElement('input');
|
||
slider.type = 'range';
|
||
slider.min = String(min);
|
||
slider.max = String(max);
|
||
slider.value = String(value);
|
||
slider.style.width = '150px';
|
||
slider.oninput = () => {
|
||
lbl.textContent = `${label}: ${slider.value}`;
|
||
onChange(Number(slider.value));
|
||
};
|
||
wrap.append(lbl, slider);
|
||
return wrap;
|
||
};
|
||
|
||
const toRad = (deg: number) => deg * Math.PI / 180;
|
||
const toDeg = (rad: number) => Math.round(rad * 180 / Math.PI);
|
||
|
||
container.append(
|
||
createSlider('angle', toDeg(light.angle), 1, 180, v => { light.angle = toRad(v); this.updateCone?.(); }),
|
||
createSlider('innerAngle', toDeg(light.innerAngle), 0, 180, v => { light.innerAngle = toRad(v); })
|
||
);
|
||
|
||
document.body.appendChild(container);
|
||
}
|
||
}
|