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); } }