import { _decorator, Camera, screen, Material, Node, Vec3, Vec4, Vec2, renderer, utils, primitives, MeshRenderer, Mesh, Layers, view, math, UITransform } from "cc"; /** * 2D简易光照-管理器 */ export class SceneLightMgr { static isMesh: boolean = false; static material: Material; static matPass: renderer.Pass; static passHandle: {[name: string]: number} = {} static width: number = 1280; static height: number = 720; static scaleX: number = 1; static camera: Camera; static uiCamera: Camera; static topSpriteNode: Node; static clear(){ this.camera = null; this.uiCamera = null; this.topSpriteNode = null; this.passHandle = {}; this.material = null this.matPass = null; } //#region 初始化部分 static initMat(material: Material, camera: Camera, topSpriteNode: Node = null, uiCamera: Camera = null) { this.camera = camera; this.uiCamera = uiCamera || camera; this.topSpriteNode = topSpriteNode; if (topSpriteNode) { const uiTransform = topSpriteNode.getComponent(UITransform); if (uiTransform) { const scale = topSpriteNode.getScale(); this.width = uiTransform.contentSize.width * scale.x; this.height = uiTransform.contentSize.height * scale.y; } else { this.width = screen.windowSize.width; this.height = screen.windowSize.height; } } else { this.width = screen.windowSize.width; this.height = screen.windowSize.height; } this.scaleX = view.getScaleX(); if (material) { this.material = material; this.material.setProperty("wh_ratio", 1); this.matPass = material.passes[0]; let nameList = ["lights", "lightShadow", "occluders"] for (let i = 0; i < nameList.length; i++) { const name = nameList[i]; this.passHandle[name] = this.matPass.getHandle(name) } this.isMesh = material.name == "lightSceneMesh"; } else { this.material = null; this.isMesh = false; } this.lightPosList = {}; } //#endregion 初始化部分 //#region 组件记录 static compList = []; static addComp(comp: any) { this.compList.push(comp); } static cleanComp(comp: any) { for (let i = this.compList.length; i >= 0; i--) { if (this.compList[i] == comp) { this.compList.splice(i, 1); } } } //#endregion 组件记录 //#region 光源部分-点光源 static lightPosList: {[uuid: string]: Vec4}= {}; static lightShadow: {[uuid: string]: Vec4} = {} static lightWorldPosList: {[uuid: string]: Vec4} = {} static addLightPos(uuid: string, pos: Vec3, radius: number, outLen: number, isShadow: boolean = false) { if(!this.topSpriteNode || !this.topSpriteNode.active){ console.log('topSpriteNode not active') return } // 保存世界坐标 if (this.lightWorldPosList[uuid]) { this.lightWorldPosList[uuid].set(pos.x, pos.y, radius, outLen); } else { this.lightWorldPosList[uuid] = this.getVec4(pos.x, pos.y, radius, outLen); } let x: number, y: number, z: number, w: number; // 优先使用“屏幕坐标空间”做统一换算(避免 UI 锚点/层级缩放/父节点偏移造成的圆心偏移) if (this.topSpriteNode) { const uiTransform = this.topSpriteNode.getComponent(UITransform); if (uiTransform && this.uiCamera) { const lightScreen = this.camera.worldToScreen(pos); const rectW = uiTransform.getBoundingBoxToWorld(); const minScreen = this.uiCamera.worldToScreen(this._tmpVec3.set(rectW.x, rectW.y, 0)); const maxScreen = this.uiCamera.worldToScreen(this._tmpVec3.set(rectW.x + rectW.width, rectW.y + rectW.height, 0)); const left = Math.min(minScreen.x, maxScreen.x); const right = Math.max(minScreen.x, maxScreen.x); const bottom = Math.min(minScreen.y, maxScreen.y); const top = Math.max(minScreen.y, maxScreen.y); const wScreen = Math.max(1, right - left); const hScreen = Math.max(1, top - bottom); const scaleFactor = Math.max(wScreen, hScreen); x = (lightScreen.x - left) / wScreen; // y 轴与 shader 保持一致:向下为正(与之前的 1 - screenY/height 一致) y = 1 - (lightScreen.y - bottom) / hScreen; z = radius / scaleFactor; w = outLen / scaleFactor; } else { let screenPos = this.camera.worldToScreen(pos); x = screenPos.x/this.width; y = 1-screenPos.y/this.height; z = this.scaleX * radius/this.height; w = this.scaleX * outLen/this.height; } } else { let screenPos = this.camera.worldToScreen(pos); x = screenPos.x/this.width; y = 1-screenPos.y/this.height; z = this.scaleX * radius/this.height; w = this.scaleX * outLen/this.height; } if (this.lightPosList[uuid]) { this.lightPosList[uuid].set(x, y, z, w); this.lightShadow[uuid].set(isShadow ? 1 : 0); } else { this.lightPosList[uuid] = this.getVec4(x, y, z, w); this.lightShadow[uuid] = this.getVec4(isShadow ? 1 : 0); } if (this.isMesh) { if (isShadow) { // 产生阴影的灯才需要重算光照mesh this.updateLightAreaOcc(uuid); this.updateLightMesh(uuid); } else { // 不产生阴影的灯 更新位置~ if (!this.lightMeshMap.has(uuid)) { // 未创建-确定mesh this.updateLightMesh(uuid); } else { // 已创建-更新位置 const lightMesh = this.lightMeshMap.get(uuid); lightMesh.node.setWorldPosition(pos.x, pos.y, 0); } } } else { this.updateMaterial(); } } static removeLight(uuid: string) { if (this.lightPosList[uuid]) { this.cacheVec4(this.lightPosList[uuid]) } if (this.lightShadow[uuid]) { this.cacheVec4(this.lightShadow[uuid]) } if (this.lightWorldPosList[uuid]) { this.cacheVec4(this.lightWorldPosList[uuid]) } this.lightPosList[uuid] = null; this.lightShadow[uuid] = null; this.lightWorldPosList[uuid] = null; this._light2Occ[uuid] = null; if (this.isMesh) { if (this.lightMeshMap.has(uuid)) { const lightMesh = this.lightMeshMap.get(uuid); if (lightMesh && lightMesh.node) { lightMesh.node.removeFromParent(); lightMesh.node.destroy(); } this.lightMeshMap.delete(uuid); } } else { this.updateMaterial(); } } static updateMaterial() { if (this.isMesh) return; if (this.material) { let newArr = []; let shadowArr = []; for (const key in this.lightPosList) { const element = this.lightPosList[key]; if (element) { newArr.push(element); shadowArr.push(this.lightShadow[key]); } } const endV = this.getVec4(-1,-1,-1,-1); newArr.push(endV); // this.material.setProperty("lights", newArr); // this.material.setProperty("lightShadow", shadowArr); this.matPass.setUniformArray(this.passHandle["lights"], newArr); this.matPass.setUniformArray(this.passHandle["lightShadow"], shadowArr); this.cacheVec4(endV); } } //#endregion 光源部分-点光源 //#region 遮挡物部分 static occluderList: {[uuid: string]: Vec4[]} = {}; static occluderRectList: {[uuid: string]: Vec4} = {}; static occluderWorldPosList: {[uuid: string]: Vec2[]} = {}; private static _tmpVec3 = new Vec3(); private static worldToUV(pos: Vec2) { this._tmpVec3.set(pos.x, pos.y, 0); let screenPos = this.camera.worldToScreen(this._tmpVec3); return this.getVec2(screenPos.x/this.width, 1-screenPos.y/this.height); } static addOccluder(uuid: string, posList: readonly Readonly[]) { //世界坐标数据 let rect: Vec4 = this.getVec4(0); if (this.occluderWorldPosList[uuid]) { this.cacheVec2s(this.occluderWorldPosList[uuid]); } this.occluderWorldPosList[uuid] = []; for (let i = 0; i < posList.length; i++) { const pos = posList[i]; this.occluderWorldPosList[uuid].push(this.getVec2(pos.x, pos.y)); if (i == 0) { rect.set(pos.x, pos.y, pos.x, pos.y); } else { rect.x = Math.min(rect.x, pos.x); rect.y = Math.min(rect.y, pos.y); rect.z = Math.max(rect.z, pos.x); rect.w = Math.max(rect.w, pos.y); } } this.occluderRectList[uuid] = rect; // 如果是mesh情况 不需要uv数据 if (this.isMesh) { this._onOccUpdatePos(uuid); return; } // uv数据 let arr: Vec4[] = []; for (let i = 0; i < posList.length; i++) { let pos1 = this.worldToUV(posList[i]); if (i == 0) { arr.push(this.getVec4(Math.ceil(posList.length/2), (posList.length-1)%2, pos1.x, pos1.y)); } else if (i + 1 < posList.length){ let pos2 = this.worldToUV(posList[i+1]); arr.push(this.getVec4(pos1.x, pos1.y, pos2.x, pos2.y)); i++; this.cacheVec2(pos2); } else { arr.push(this.getVec4(pos1.x, pos1.y)); i++; } this.cacheVec2(pos1); } this.occluderList[uuid] = arr; this.updateMatOccluder(); } static removeOccluder(uuid: string) { if (this.occluderList[uuid]) { this.cacheVec4s(this.occluderList[uuid]) } if (this.occluderWorldPosList[uuid]) { this.cacheVec2s(this.occluderWorldPosList[uuid]); } this.occluderWorldPosList[uuid] = null; this.occluderList[uuid] = null; this.updateMatOccluder(); if (this.isMesh) { for (const lightId in this._light2Occ) { const list = this._light2Occ[lightId] if (list && list.indexOf(uuid) >= 0) { let id = list.findIndex(v => v == uuid) list.splice(id, 1); this.updateLightMesh(lightId); } } } } static updateMatOccluder() { if (this.isMesh) return; // 如果是mesh情况 不需要 if (this.material) { let newArr = []; for (const key in this.occluderList) { const element = this.occluderList[key]; if (element) { for (let index = 0; index < element.length; index++) { newArr.push(element[index]); } } } const endV = this.getVec4(-1); newArr.push(endV); // 避免因为节点隐藏导致计算还保留之前的值 // this.material.setProperty("occluders", newArr); this.matPass.setUniformArray(this.passHandle["occluders"], newArr); this.cacheVec4(endV); } } //#endregion 遮挡物部分 //#region Vec4 cache private static _vec4Cache: Vec4[] = []; private static getVec4(x?: number, y?: number, z?: number, w?: number): Vec4 { if (this._vec4Cache.length > 0) { let vec4 = this._vec4Cache.pop(); return vec4.set(x, y, z, w); } return new Vec4(x, y, z, w); } private static cacheVec4(vec4: Vec4) { this._vec4Cache.push(vec4); } private static cacheVec4s(vec4Arr: Vec4[]) { for (let index = 0; index < vec4Arr.length; index++) { this._vec4Cache.push(vec4Arr[index]); } } //#endregion Vec4 cache //#region Vec2 cache private static _vec2Cache: Vec2[] = []; private static getVec2(x?: number, y?: number): Vec2 { if (this._vec2Cache.length > 0) { let vec2 = this._vec2Cache.pop(); return vec2.set(x, y); } return new Vec2(x, y); } private static cacheVec2(vec2: Vec2) { this._vec2Cache.push(vec2); } private static cacheVec2s(vec2Arr: Vec2[]) { for (let index = 0; index < vec2Arr.length; index++) { this._vec2Cache.push(vec2Arr[index]); } } //#endregion Vec4 cache //#region 光照mesh-相关计算 // 记录光照内有哪些遮挡物 private static _light2Occ: {[uuid: string]: string[]} = {}; // 重新计算某光照范围内的遮挡物 private static updateLightAreaOcc(uuid: string) { const lightData = this.lightWorldPosList[uuid]; const r = (lightData.z + lightData.w); const lightRect = this.getVec4(lightData.x - r, lightData.y - r, lightData.x + r, lightData.y + r) this._light2Occ[uuid] = []; for (const occUUID in this.occluderRectList) { const rect = this.occluderRectList[occUUID]; if (rect && this._crossRect(lightRect, rect)) { this._light2Occ[uuid].push(occUUID); } } this.cacheVec4(lightRect); } // 判断矩形是否相交 private static _crossRect(v1: Vec4, v2: Vec4): boolean { return !(v1.x > v2.z || v1.z < v2.x || v1.y > v2.w || v1.w < v2.y); } private static _onOccUpdatePos(occUUID: string) { let effLights = [] // 计算新位置影响的光源 const rect = this.occluderRectList[occUUID]; for (const lightId in this.lightWorldPosList) { const lightData = this.lightWorldPosList[lightId]; if (!lightData) continue; const isShadow = this.lightShadow[lightId].x >= 0.5; if (!isShadow) continue; const r = (lightData.z + lightData.w); const lightRect = this.getVec4(lightData.x - r, lightData.y - r, lightData.x + r, lightData.y + r) if (this._crossRect(lightRect, rect)) { effLights.push(lightId); } else { let list = this._light2Occ[lightId]; if (list && list.indexOf(occUUID) >= 0) { effLights.push(lightId); } } } // for (let i = 0; i < effLights.length; i++) { const lightId = effLights[i]; this.updateLightAreaOcc(lightId); this.updateLightMesh(lightId); } } /** * 获取灯光范围内的遮挡物 的 全部线段 */ private static _getAllOccSeg(lightId: string): {start: Vec2, end: Vec2}[] { const list = this._light2Occ[lightId]; const segList = []; if (list && list.length > 0) { for (let i = 0; i < list.length; i++) { const occData = this.occluderWorldPosList[list[i]]; if (!occData) continue; for (let j = 0; j < occData.length; j++) { let nextId = (j + 1) % occData.length; segList.push({ start: this.getVec2(occData[j].x, occData[j].y), end: this.getVec2(occData[nextId].x, occData[nextId].y) }) } } } return segList; } private static getRayMinPos(origin: Vec2, direction: Vec2, maxDis: number, segList: {start: Vec2, end: Vec2}[]) { let minPos: Vec2 = this.getVec2(0, 0); let minDis: number = Infinity; let tempV2: Vec2 = this.getVec2(0, 0); let isFind: boolean = false; for (let i = 0; i < segList.length; i++) { const seg = segList[i]; const inter = this.getRayLineInter(origin, direction, seg.start, seg.end, maxDis); if (inter) { tempV2.set(inter.x - origin.x, inter.y - origin.y); const dis = tempV2.length(); if (dis < minDis) { isFind = true; minDis = dis; minPos.set(inter.x, inter.y); } this.cacheVec2(inter); } } this.cacheVec2(tempV2); return isFind ? minPos : null; } private static getRayLineInter(origin: Vec2, direction: Vec2, p1: Vec2, p2: Vec2, maxDis: number): Vec2 | null { const a11 = direction.x; const a12 = -(p2.x - p1.x); const a21 = direction.y; const a22 = -(p2.y - p1.y); const determinant = a11 * a22 - a12 * a21; if (Math.abs(determinant) < 1e-6 ) return null; // 平行或重合 const b1 = p1.x - origin.x; const b2 = p1.y - origin.y; const t = (a22 * b1 - a12 * b2) / determinant; const s = (a11 * b2 - a21 * b1) / determinant; if (t >= 0 && t <= maxDis && s >= 0 && s <= 1) { return this.getVec2( origin.x + t * direction.x, origin.y + t * direction.y ); } return null; } //#endregion 光照mesh-相关计算 //#region 光照mesh-创建和更新 public static meshParentNode: Node; public static meshMaterial: Material; private static lightMeshMap = new Map(); private static lightMeshLayer = 1 << Layers.nameToLayer("LightTest"); private static createMeshNode(lightId: string) { const node: Node = new Node(lightId); node.parent = this.meshParentNode; node.layer = this.lightMeshLayer; let renderer: MeshRenderer = node.addComponent(MeshRenderer); renderer.material = this.meshMaterial; renderer.receiveShadowForInspector = false; let lightData = this.lightWorldPosList[lightId]; let mat = renderer.getMaterialInstance(0) mat.setProperty('radius', lightData.z); mat.setProperty('outlen', lightData.w); const vertices: Float32Array = new Float32Array(1); const indices: Uint16Array = new Uint16Array(1); let geometry: primitives.IDynamicGeometry = { positions: vertices, indices16: indices, }; let ptions: primitives.ICreateDynamicMeshOptions = { maxSubMeshes: 1, maxSubMeshVertices: 4096, maxSubMeshIndices: 4096 }; // 创建参数 不修改时候顶点超过341个就报错了 (2048/6=341.33?) let mesh = utils.MeshUtils.createDynamicMesh(0, geometry, null, ptions); renderer.mesh = mesh; this.lightMeshMap.set(lightId, {node, renderer, mesh, geometry}); } private static updateMesh(lightId: string, posList: Vec2[]) { if (!this.lightMeshMap.has(lightId)) { this.createMeshNode(lightId); } const lightMesh = this.lightMeshMap.get(lightId); //位置更新 const worldPos = this.lightWorldPosList[lightId]; lightMesh.node.setWorldPosition(worldPos.x, worldPos.y, 0); // 最后一个点为圆心 const posLen = posList.length; const posLen2 = posLen - 1; let vertices: Float32Array = new Float32Array(posLen*3); let indices: Uint16Array = new Uint16Array((posLen - 1) * 3); for (let i = 0; i < posLen; i++) { const id = i * 3; vertices[id] = posList[i].x; vertices[id + 1] = posList[i].y; vertices[id + 2] = 0; if (i != posLen2) { indices[id] = i; indices[id + 1] = (i + 1) % posLen2; indices[id + 2] = posLen2; } } lightMesh.geometry.positions = vertices; lightMesh.geometry.indices16 = indices; // 刷新 lightMesh.mesh.updateSubMesh(0, lightMesh.geometry); lightMesh.renderer.onGeometryChanged(); } static updateLightMesh(lightId: string) { const lightData = this.lightWorldPosList[lightId]; if (!lightData) return; const origin = this.getVec2(lightData.x, lightData.y); const maxDis = lightData.z + lightData.w; // 获取范围内遮挡物线段 const segList = this._getAllOccSeg(lightId); // this._calOccLinePos(lightId, origin, maxDis, segList) // for (let index = 0; index < segList.length; index++) { this.cacheVec2(segList[index].start); this.cacheVec2(segList[index].end); } this.cacheVec2(origin); } // 计算圆心和线段相加点 private static _calOccLinePos(lightId: string, origin: Vec2, maxDis: number, segList: {start: Vec2, end: Vec2}[]) { let rayDir: Vec2 = this.getVec2(0, 0); let exDirList: number[] = []; // 额外需要计算的角度 function addExAngle(pos: Vec2) { let angle = math.toDegree(Math.atan2(pos.y - origin.y, pos.x - origin.x)); if (angle < 0) angle += 360; exDirList.push(angle); exDirList.push(angle - 0.01); exDirList.push(angle + 0.01); } for (let i = 0; i < segList.length; i++) { const seg = segList[i]; addExAngle(seg.start); } // 去除重复的角度 if (exDirList.length > 0) { exDirList.sort((a, b) => a-b); for (let index = exDirList.length-1; index > 0; index--) { if (Math.abs(exDirList[index] - exDirList[index-1]) < 1e-6) { exDirList.splice(index, 1); } } } if (this._rDirectList.length <= 0) this._initRDirectList(); // let posList = [] if (exDirList.length > 0) { // 有额外角度需要计算 let id1 = 0; let id2 = 0; function addDefault() { const data = SceneLightMgr._rDirectList[id1]; let pos = SceneLightMgr.getRayMinPos(origin, data.dir, maxDis, segList); if (pos) { posList.push(SceneLightMgr.getVec2(pos.x - origin.x, pos.y - origin.y)); SceneLightMgr.cacheVec2(pos); } else { posList.push(SceneLightMgr.getVec2(data.dir.x * maxDis, data.dir.y * maxDis)) } id1++; } function addExtra() { const rad = math.toRadian(exDirList[id2]); const dir = SceneLightMgr.getVec2(Math.cos(rad), Math.sin(rad)) let pos = SceneLightMgr.getRayMinPos(origin, dir, maxDis, segList); if (pos) { posList.push(SceneLightMgr.getVec2(pos.x - origin.x, pos.y - origin.y)) SceneLightMgr.cacheVec2(pos); } else { posList.push(SceneLightMgr.getVec2(dir.x * maxDis, dir.y * maxDis)) } id2++; } while (id1 < this._rDirectList.length || id2 < exDirList.length) { if (id1 >= this._rDirectList.length) { addExtra(); } else if (id2 >= exDirList.length) { addDefault(); } else { const angle1 = this._rDirectList[id1].angle; const angle2 = exDirList[id2]; if (Math.abs(angle1 - angle2) < 1e-6) { addDefault(); id2++; } else if (angle1 < angle2) { addDefault(); } else { addExtra(); } } } } else { // 光照区域无遮挡物 for (let i = 0; i < this._rDirectList.length; i++) { const data = this._rDirectList[i]; posList.push(this.getVec2(data.dir.x * maxDis, data.dir.y * maxDis)) } } posList.push(this.getVec2(0, 0)); this.updateMesh(lightId, posList); this.cacheVec2s(posList); this.cacheVec2(rayDir); } private static _rDirStep: number = 100; private static readonly _rDirectList: {dir: Vec2, angle: number}[] = []; // 初始射线方向列表 private static _initRDirectList() { const rr = 2 * Math.PI / this._rDirStep; for (let i = 0; i < this._rDirStep; i++) { const degree = i * rr; const angle = math.toDegree(degree); const dir = this.getVec2(Math.cos(degree), Math.sin(degree)) this._rDirectList.push({dir: dir, angle: angle}) } } /** * 当 TopSprite (光照遮罩层) 移动时调用此方法 * 修复了 Y 轴翻转和坐标偏移问题 */ static updateByTopSpriteMove(topSpriteNode: Node) { if (!this.material || this.isMesh) return; const uiTransform = topSpriteNode.getComponent(UITransform); if (!uiTransform) return; // 获取 TopSprite 的实际尺寸(使用 contentSize,与 convertToNodeSpaceAR 的局部坐标单位保持一致) const ws = topSpriteNode.worldScale; const width = uiTransform.contentSize.width * ws.x; const height = uiTransform.contentSize.height * ws.y; // 设置 wh_ratio 为 1,确保光源显示为圆形 this.material.setProperty("wh_ratio", 1); // 辅助函数:将世界坐标转换为 TopSprite 内部的 UV const getUVFromWorldPos = (worldPos: Vec3) => { // 将世界坐标转换为 TopSprite 内部的局部坐标 const localPos = uiTransform.convertToNodeSpaceAR(worldPos); // 转换为 UV 坐标(使用相同的缩放因子,确保光源显示为圆形) const scaleFactor = Math.max(width, height); const anchor = uiTransform.anchorPoint; const cx = (localPos.x + (0.5 - anchor.x) * uiTransform.contentSize.width) * ws.x; const cy = (localPos.y + (0.5 - anchor.y) * uiTransform.contentSize.height) * ws.y; const u = 0.5 + cx / scaleFactor; const v = 0.5 - cy / scaleFactor; return { u, v }; }; // --- 更新点光源 --- for (const uuid in this.lightWorldPosList) { const data = this.lightWorldPosList[uuid]; if (!data) continue; this._tmpVec3.set(data.x, data.y, 0); const uv = getUVFromWorldPos(this._tmpVec3); // 半径转换 (世界单位 -> UV 单位) // 使用统一的缩放因子,确保光源显示为圆形 const scaleFactor = Math.max(width, height); const radiusUV = data.z / scaleFactor; const outLenUV = data.w / scaleFactor; if (this.lightPosList[uuid]) { this.lightPosList[uuid].set(uv.u, uv.v, radiusUV, outLenUV); } } this.updateMaterial(); // --- 更新遮挡物 --- for (const uuid in this.occluderWorldPosList) { const worldPosList = this.occluderWorldPosList[uuid]; if (!worldPosList) continue; let arr: Vec4[] = []; for (let i = 0; i < worldPosList.length; i++) { const wp = worldPosList[i]; this._tmpVec3.set(wp.x, wp.y, 0); const uv = getUVFromWorldPos(this._tmpVec3); if (i == 0) { arr.push(this.getVec4(Math.ceil(worldPosList.length/2), (worldPosList.length-1)%2, uv.u, uv.v)); } else if (i + 1 < worldPosList.length){ const wpNext = worldPosList[i+1]; this._tmpVec3.set(wpNext.x, wpNext.y, 0); const uvNext = getUVFromWorldPos(this._tmpVec3); arr.push(this.getVec4(uv.u, uv.v, uvNext.u, uvNext.v)); i++; } else { arr.push(this.getVec4(uv.u, uv.v)); i++; } } this.occluderList[uuid] = arr; } this.updateMatOccluder(); } /** * 初始化光源时调用此方法,确保光源显示为圆形 * @param topSpriteNode night节点 */ static initLightsForTopSprite(topSpriteNode: Node) { if (!this.material || this.isMesh) return; const uiTransform = topSpriteNode.getComponent(UITransform); if (!uiTransform) return; // 获取 TopSprite 的实际尺寸(使用 contentSize,与 convertToNodeSpaceAR 的局部坐标单位保持一致) const ws = topSpriteNode.worldScale; const width = uiTransform.contentSize.width * ws.x; const height = uiTransform.contentSize.height * ws.y; // 设置 wh_ratio 为 1,确保光源显示为圆形 this.material.setProperty("wh_ratio", 1); // 辅助函数:将世界坐标转换为 TopSprite 内部的 UV const getUVFromWorldPos = (worldPos: Vec3) => { // 将世界坐标转换为 TopSprite 内部的局部坐标 const localPos = uiTransform.convertToNodeSpaceAR(worldPos); // 转换为 UV 坐标(使用相同的缩放因子,确保光源显示为圆形) const scaleFactor = Math.max(width, height); const anchor = uiTransform.anchorPoint; const cx = (localPos.x + (0.5 - anchor.x) * uiTransform.contentSize.width) * ws.x; const cy = (localPos.y + (0.5 - anchor.y) * uiTransform.contentSize.height) * ws.y; const u = 0.5 + cx / scaleFactor; const v = 0.5 - cy / scaleFactor; return { u, v }; }; // 更新所有光源 for (const uuid in this.lightWorldPosList) { const data = this.lightWorldPosList[uuid]; if (!data) continue; this._tmpVec3.set(data.x, data.y, 0); const uv = getUVFromWorldPos(this._tmpVec3); // 半径转换 (世界单位 -> UV 单位) // 使用统一的缩放因子,确保光源显示为圆形 const scaleFactor = Math.max(width, height); const radiusUV = data.z / scaleFactor; const outLenUV = data.w / scaleFactor; if (this.lightPosList[uuid]) { this.lightPosList[uuid].set(uv.u, uv.v, radiusUV, outLenUV); } } this.updateMaterial(); } //#endregion //#endregion 光照mesh-创建和更新 }