WebGL 法线数据详解
1. 法线数据的作用
1.1 光照计算
法线是光照计算的核心:
- 决定光线与表面的夹角
- 影响漫反射和镜面反射强度
- 控制高光的位置和形状
源码中的光照计算:
// src/renderers/shaders/ShaderChunk/lights_physical_fragment.glsl.js
vec3 normal = normalize(vNormal);
float NdotL = dot(normal, lightDirection);
float diffuse = max(0.0, NdotL);
1.2 表面细节模拟
法线贴图技术:
- 在不增加几何复杂度的情况下增加表面细节
- 通过修改法线方向模拟凹凸效果
- 广泛应用于游戏和实时渲染
1.3 视觉效果增强
法线数据支持的视觉效果:
- 平滑着色 (Smooth Shading)
- 法线贴图 (Normal Mapping)
- 环境映射 (Environment Mapping)
- 菲涅尔效果 (Fresnel Effect)
2. 法线数据的分类
2.1 按计算方式分类
2.1.1 面法线 (Face Normals)
定义: 每个三角形面的法线向量
源码实现:
// src/core/BufferGeometry.js
computeVertexNormals() {
const index = this.index;
const positionAttribute = this.getAttribute('position');
if (positionAttribute !== undefined) {
let normalAttribute = this.getAttribute('normal');
if (normalAttribute === undefined) {
normalAttribute = new BufferAttribute(
new Float32Array(positionAttribute.count * 3), 3
);
this.setAttribute('normal', normalAttribute);
}
// 计算每个面的法线
for (let i = 0, il = index.count; i < il; i += 3) {
const vA = index.getX(i + 0);
const vB = index.getX(i + 1);
const vC = index.getX(i + 2);
// 获取三个顶点位置
pA.fromBufferAttribute(positionAttribute, vA);
pB.fromBufferAttribute(positionAttribute, vB);
pC.fromBufferAttribute(positionAttribute, vC);
// 计算面法线: (C-B) × (A-B)
cb.subVectors(pC, pB);
ab.subVectors(pA, pB);
cb.cross(ab);
// 累加到顶点法线
nA.fromBufferAttribute(normalAttribute, vA);
nB.fromBufferAttribute(normalAttribute, vB);
nC.fromBufferAttribute(normalAttribute, vC);
nA.add(cb);
nB.add(cb);
nC.add(cb);
normalAttribute.setXYZ(vA, nA.x, nA.y, nA.z);
normalAttribute.setXYZ(vB, nB.x, nB.y, nB.z);
normalAttribute.setXYZ(vC, nC.x, nC.y, nC.z);
}
this.normalizeNormals();
}
}
2.1.2 顶点法线 (Vertex Normals)
定义: 每个顶点的法线向量,通常是相邻面法线的平均值
计算过程:
- 计算所有相邻面的法线
- 对法线进行加权平均
- 归一化结果
源码实现:
// src/core/BufferGeometry.js
normalizeNormals() {
const normals = this.attributes.normal;
for (let i = 0, il = normals.count; i < il; i++) {
_vector.fromBufferAttribute(normals, i);
_vector.normalize();
normals.setXYZ(i, _vector.x, _vector.y, _vector.z);
}
}
2.2 按坐标系分类
2.2.1 切线空间法线 (Tangent Space Normals)
定义: 相对于表面切线空间的法线
特点:
- 法线贴图的标准格式
- 独立于模型变换
- 支持表面细节模拟
源码实现:
// src/constants.js
export const TangentSpaceNormalMap = 0;
// src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl.js
#ifdef USE_NORMALMAP_TANGENTSPACE
vec3 mapN = texture2D(normalMap, vNormalMapUv).xyz * 2.0 - 1.0;
mapN.xy *= normalScale;
normal = normalize(tbn * mapN);
#endif
切线空间计算:
// src/renderers/shaders/ShaderChunk/normalmap_pars_fragment.glsl.js
mat3 getTangentFrame(vec3 eye_pos, vec3 surf_norm, vec2 uv) {
vec3 q0 = dFdx(eye_pos.xyz);
vec3 q1 = dFdy(eye_pos.xyz);
vec2 st0 = dFdx(uv.st);
vec2 st1 = dFdy(uv.st);
vec3 N = surf_norm; // 法线
vec3 q1perp = cross(q1, N);
vec3 q0perp = cross(N, q0);
vec3 T = q1perp * st0.x + q0perp * st1.x; // 切线
vec3 B = q1perp * st0.y + q0perp * st1.y; // 副切线
float det = max(dot(T, T), dot(B, B));
float scale = (det == 0.0) ? 0.0 : inversesqrt(det);
return mat3(T * scale, B * scale, N);
}
2.2.2 对象空间法线 (Object Space Normals)
定义: 相对于模型对象空间的法线
特点:
- 直接使用模型坐标系
- 需要法线矩阵变换
- 适用于简单场景
源码实现:
// src/constants.js
export const ObjectSpaceNormalMap = 1;
// src/renderers/shaders/ShaderChunk/normal_fragment_maps.glsl.js
#ifdef USE_NORMALMAP_OBJECTSPACE
normal = texture2D(normalMap, vNormalMapUv).xyz * 2.0 - 1.0;
#ifdef FLIP_SIDED
normal = -normal;
#endif
#ifdef DOUBLE_SIDED
normal = normal * faceDirection;
#endif
normal = normalize(normalMatrix * normal);
#endif
2.3 按存储格式分类
2.3.1 几何法线 (Geometry Normals)
存储位置: BufferGeometry.attributes.normal
格式: Float32Array,每个顶点3个分量 (x, y, z)
源码定义:
// src/core/BufferGeometry.js
class BufferGeometry {
constructor() {
this.attributes = {};
// normal 属性存储顶点法线
}
setAttribute(name, attribute) {
this.attributes[name] = attribute;
}
getAttribute(name) {
return this.attributes[name];
}
}
2.3.2 法线贴图 (Normal Maps)
存储位置: Material.normalMap
格式: 纹理贴图,RGB通道存储法线信息
源码实现:
// src/renderers/webgl/WebGLPrograms.js
const HAS_NORMALMAP = !!material.normalMap;
// 法线贴图参数
parameters = {
normalMap: HAS_NORMALMAP,
normalMapObjectSpace: HAS_NORMALMAP && material.normalMapType === ObjectSpaceNormalMap,
normalMapTangentSpace: HAS_NORMALMAP && material.normalMapType === TangentSpaceNormalMap,
normalMapUv: HAS_NORMALMAP && getChannel(material.normalMap.channel),
normalScale: material.normalScale
};
3. 法线数据的应用场景
3.1 基础光照模型
Lambert 漫反射:
// src/renderers/shaders/ShaderChunk/lights_lambert_fragment.glsl.js
vec3 normal = normalize(vNormal);
float NdotL = dot(normal, lightDirection);
float diffuse = max(0.0, NdotL);
vec3 diffuseColor = diffuse * lightColor;
Phong 镜面反射:
// src/renderers/shaders/ShaderChunk/lights_phong_fragment.glsl.js
vec3 normal = normalize(vNormal);
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
vec3 reflectDirection = reflect(-lightDirection, normal);
float specular = pow(max(dot(viewDirection, reflectDirection), 0.0), shininess);
3.2 法线贴图应用
切线空间法线贴图:
// 材质设置
const material = new THREE.MeshStandardMaterial({
normalMap: normalTexture,
normalMapType: THREE.TangentSpaceNormalMap,
normalScale: new THREE.Vector2(1, 1)
});
// 着色器处理
#ifdef USE_NORMALMAP_TANGENTSPACE
vec3 mapN = texture2D(normalMap, vNormalMapUv).xyz * 2.0 - 1.0;
mapN.xy *= normalScale;
normal = normalize(tbn * mapN);
#endif
对象空间法线贴图:
// 材质设置
const material = new THREE.MeshStandardMaterial({
normalMap: normalTexture,
normalMapType: THREE.ObjectSpaceNormalMap
});
// 着色器处理
#ifdef USE_NORMALMAP_OBJECTSPACE
normal = texture2D(normalMap, vNormalMapUv).xyz * 2.0 - 1.0;
normal = normalize(normalMatrix * normal);
#endif
3.3 环境映射
环境反射:
// src/renderers/shaders/ShaderChunk/envmap_fragment.glsl.js
vec3 normal = normalize(vNormal);
vec3 reflectDirection = reflect(viewDirection, normal);
vec3 envColor = textureCube(envMap, reflectDirection).rgb;
环境折射:
vec3 normal = normalize(vNormal);
vec3 refractDirection = refract(viewDirection, normal, refractionRatio);
vec3 envColor = textureCube(envMap, refractDirection).rgb;
4. 法线数据的优化策略
4.1 法线压缩
压缩存储:
// 使用 16 位浮点数存储法线
const normalAttribute = new THREE.BufferAttribute(
new Float16Array(normalData), 3
);
法线量化:
// 将法线量化到 8 位
function quantizeNormal(normal) {
return {
x: Math.round(normal.x * 127) / 127,
y: Math.round(normal.y * 127) / 127,
z: Math.round(normal.z * 127) / 127
};
}
4.2 法线生成优化
增量法线计算:
// 只计算变化的部分
function updateNormals(geometry, changedFaces) {
const normalAttribute = geometry.getAttribute('normal');
changedFaces.forEach(faceIndex => {
// 只更新受影响的面
updateFaceNormal(faceIndex, normalAttribute);
});
}
法线缓存:
// 缓存计算好的法线
const normalCache = new Map();
function getCachedNormals(geometry) {
const key = geometry.uuid;
if (!normalCache.has(key)) {
normalCache.set(key, computeNormals(geometry));
}
return normalCache.get(key);
}
4.3 法线贴图优化
法线贴图压缩:
// 使用压缩纹理格式
const normalTexture = new THREE.TextureLoader().load('normal.jpg');
normalTexture.format = THREE.RGBFormat;
normalTexture.minFilter = THREE.LinearMipmapLinearFilter;
normalTexture.magFilter = THREE.LinearFilter;
法线贴图 LOD:
// 根据距离选择不同精度的法线贴图
function getNormalMapLOD(distance) {
if (distance < 10) return highResNormalMap;
if (distance < 50) return mediumResNormalMap;
return lowResNormalMap;
}
5. 法线数据的调试技巧
5.1 法线可视化
法线颜色映射:
// 将法线映射到颜色
function visualizeNormals(geometry) {
const normalAttribute = geometry.getAttribute('normal');
const colorAttribute = new THREE.BufferAttribute(
new Float32Array(normalAttribute.count * 3), 3
);
for (let i = 0; i < normalAttribute.count; i++) {
const normal = new THREE.Vector3();
normal.fromBufferAttribute(normalAttribute, i);
// 法线范围 [-1,1] 映射到颜色范围 [0,1]
colorAttribute.setXYZ(i,
normal.x * 0.5 + 0.5,
normal.y * 0.5 + 0.5,
normal.z * 0.5 + 0.5
);
}
geometry.setAttribute('color', colorAttribute);
}
法线箭头显示:
// 使用 ArrowHelper 显示法线
function showNormals(geometry) {
const normalAttribute = geometry.getAttribute('normal');
const positionAttribute = geometry.getAttribute('position');
for (let i = 0; i < normalAttribute.count; i += 10) { // 每10个顶点显示一个
const position = new THREE.Vector3();
const normal = new THREE.Vector3();
position.fromBufferAttribute(positionAttribute, i);
normal.fromBufferAttribute(normalAttribute, i);
const arrow = new THREE.ArrowHelper(
normal, position, 0.1, 0xff0000
);
scene.add(arrow);
}
}
5.2 法线错误检测
法线长度检查:
function validateNormals(geometry) {
const normalAttribute = geometry.getAttribute('normal');
for (let i = 0; i < normalAttribute.count; i++) {
const normal = new THREE.Vector3();
normal.fromBufferAttribute(normalAttribute, i);
const length = normal.length();
if (Math.abs(length - 1.0) > 0.01) {
console.warn(`Normal at index ${i} has length ${length}, should be 1.0`);
}
}
}
法线方向检查:
function checkNormalDirections(geometry) {
const normalAttribute = geometry.getAttribute('normal');
const positionAttribute = geometry.getAttribute('position');
for (let i = 0; i < normalAttribute.count; i++) {
const normal = new THREE.Vector3();
const position = new THREE.Vector3();
normal.fromBufferAttribute(normalAttribute, i);
position.fromBufferAttribute(positionAttribute, i);
// 检查法线是否指向外部
const dotProduct = normal.dot(position.normalize());
if (dotProduct < 0) {
console.warn(`Normal at index ${i} points inward`);
}
}
}
6. 最佳实践
6.1 法线数据管理
统一法线计算:
// 确保所有几何体都有正确的法线
function ensureNormals(geometry) {
if (!geometry.getAttribute('normal')) {
geometry.computeVertexNormals();
}
// 检查法线是否需要更新
if (geometry.attributes.normal.needsUpdate) {
geometry.computeVertexNormals();
}
}
法线数据验证:
function validateGeometry(geometry) {
const normalAttribute = geometry.getAttribute('normal');
if (!normalAttribute) {
console.error('Geometry missing normal attribute');
return false;
}
if (normalAttribute.itemSize !== 3) {
console.error('Normal attribute should have itemSize 3');
return false;
}
return true;
}
6.2 性能优化建议
法线计算优化:
// 只在需要时计算法线
function updateGeometry(geometry, needsNormals = false) {
if (needsNormals && !geometry.getAttribute('normal')) {
geometry.computeVertexNormals();
}
}
法线贴图优化:
// 使用合适的法线贴图格式
const normalTexture = new THREE.TextureLoader().load('normal.jpg');
normalTexture.wrapS = THREE.RepeatWrapping;
normalTexture.wrapT = THREE.RepeatWrapping;
normalTexture.generateMipmaps = true;
normalTexture.minFilter = THREE.LinearMipmapLinearFilter;
通过深入理解法线数据的作用和分类,开发者可以更好地利用 WebGL 的渲染能力,创建更真实和高效的 3D 图形效果。关键是要根据具体应用场景选择合适的法线类型和优化策略。
WebGL法线数据解析与应用

被折叠的 条评论
为什么被折叠?



