如何找出被THREE.SpotLight照亮的所有网格面?
实现思路
要找出被THREE.SpotLight照亮的网格面,需同时满足两个核心条件:面处于光源的锥形照射范围内,且面朝向光源(无遮挡场景下)。以下是具体实现方案:
1. 提取网格面数据
针对BufferGeometry,需按顶点索引分组获取每个面的顶点信息:
- 若几何体有
index属性,每3个索引对应一个面的三个顶点; - 若无
index属性,直接按连续3个顶点为一组处理。
2. 判断面是否在光源锥形范围内
取面的中心作为判断基准点,完成两个维度的校验:
- 距离校验:计算面中心到光源的距离,若超过
spotLight.distance(非0时),则排除; - 锥形范围校验:计算光源方向向量与面中心指向光源的向量的点积,若点积小于光源锥形半角的余弦值,说明面不在锥形内,直接排除。
3. 判断面是否朝向光源
通过面法线与光源指向面的向量的点积判断:
- 若使用平滑着色,取三个顶点法线的平均值作为面法线;若为平面着色,可直接计算面的几何法线;
- 计算面法线与「光源指向面中心的向量」的点积,若结果大于0,说明面朝向光源,满足光照条件。
代码示例
function getLitFaces(mesh, spotLight) { const geometry = mesh.geometry; const positions = geometry.attributes.position.array; const normals = geometry.attributes.normal.array; const index = geometry.index; const litFaceIndices = []; // 预计算光源的关键参数 const lightPos = spotLight.position.clone(); const lightDir = spotLight.target.position.clone().sub(lightPos).normalize(); const cosHalfAngle = Math.cos(spotLight.angle / 2); const maxDistance = spotLight.distance; // 处理网格的世界变换矩阵 const meshMatrix = mesh.matrixWorld; // 遍历所有面 const faceCount = index ? index.count / 3 : positions.length / 9; for (let i = 0; i < faceCount; i++) { let v1Idx, v2Idx, v3Idx; if (index) { v1Idx = index.array[i * 3] * 3; v2Idx = index.array[i * 3 + 1] * 3; v3Idx = index.array[i * 3 + 2] * 3; } else { v1Idx = i * 9; v2Idx = i * 9 + 3; v3Idx = i * 9 + 6; } // 获取并转换顶点到世界空间 const v1 = new THREE.Vector3(positions[v1Idx], positions[v1Idx + 1], positions[v1Idx + 2]).applyMatrix4(meshMatrix); const v2 = new THREE.Vector3(positions[v2Idx], positions[v2Idx + 1], positions[v2Idx + 2]).applyMatrix4(meshMatrix); const v3 = new THREE.Vector3(positions[v3Idx], positions[v3Idx + 1], positions[v3Idx + 2]).applyMatrix4(meshMatrix); // 计算面中心 const faceCenter = new THREE.Vector3(); faceCenter.addVectors(v1, v2).add(v3).divideScalar(3); // 校验距离是否超出范围 const distanceToLight = lightPos.distanceTo(faceCenter); if (maxDistance > 0 && distanceToLight > maxDistance) { continue; } // 校验是否在锥形范围内 const dirToFace = faceCenter.clone().sub(lightPos).normalize(); if (lightDir.dot(dirToFace) < cosHalfAngle) { continue; } // 计算面法线并校验朝向 const n1 = new THREE.Vector3(normals[v1Idx], normals[v1Idx + 1], normals[v1Idx + 2]).applyMatrix4(meshMatrix).normalize(); const n2 = new THREE.Vector3(normals[v2Idx], normals[v2Idx + 1], normals[v2Idx + 2]).applyMatrix4(meshMatrix).normalize(); const n3 = new THREE.Vector3(normals[v3Idx], normals[v3Idx + 1], normals[v3Idx + 2]).applyMatrix4(meshMatrix).normalize(); const faceNormal = new THREE.Vector3().addVectors(n1, n2).add(n3).divideScalar(3).normalize(); const lightToFace = faceCenter.clone().sub(lightPos).normalize(); if (faceNormal.dot(lightToFace.negate()) <= 0) { continue; } // 记录符合条件的面索引 litFaceIndices.push(i); } return litFaceIndices; } // 使用示例 const litFaces = getLitFaces(yourMesh, yourSpotLight); console.log("被照亮的面索引:", litFaces);
注意事项
- 必须将顶点转换到世界空间计算,因为光源位置默认是世界空间坐标;
- 若使用平面着色,可直接通过三个顶点计算面的几何法线,无需平均顶点法线,提升计算效率;
- 代码用面中心作为判断点,若需处理面部分在锥形内的场景,可额外对每个顶点做锥形范围校验。
内容的提问来源于stack exchange,提问作者Nevermore
相关产品推荐
相关产品推荐

