理解法线属性
想象一下, 我们有一个 3D 模型,比如一个光滑的球体或者一个立方体。这个模型是由很多很多小三角形(面)拼成的。
每个小三角形有三个尖角,我们管它叫"顶点"(Vertex) 所以模型上有成百上千个顶点。 现在我们想给这个模型打光(比如放个灯泡在场景里)
那么问题来了,灯光照在模型的某个点上,到底应该有多亮? 这取决于这个点面向灯光的方向!
法线(Normal)就是解决这个问题的"方向指示器"
法线是什么
法线是一个向量(有方向的箭头)。想象在每个顶点上(或者在每个三角形的中心)插着一根非常非常小的针。
它指向哪里
这根"针"(法线)垂直地指向模型表面的"外面" 。就像你用手指垂直地戳一个物体的表面,
它有什么用
当灯光照射过来时,渲染器(WebGL)会做以下计算:
- 看看灯光射过来的方向。
- 看看这个顶点表面的法线方向。
- 计算灯光方向 和法线方向 之间的夹角。
- 如果法线正对着灯光(夹角接近 0°),这个点就非常亮(比如球体正对灯泡的部分)。
- 如果法线斜对着灯光(夹角比如 45°),这个点就比较暗。
- 如果法线背对着灯光(夹角接近 180°),这个点就非常暗甚至全黑(比如球体的背光面)。
注意:如果法线错了会怎么样
- 光照完全混乱: 该亮的地方不亮,该暗的地方不暗,甚至一片漆黑(如果所有法线都指向模型内部)。
- 模型看起来"扁平"或没有立体感: 缺少明暗对比。
- "平滑"表面出现奇怪棱角: 或者有棱角的地方看起来是平滑的(这跟法线共享有关)。
如何获得法线
通常会自动计算 对于简单几何体或导入的模型,通常调用 geometry.computeVertexNormals()
来获取
手动设置 如果我们在创建非常特殊的自定义形状,或者需要精确控制光照效果(比如实现硬边),就需要自己计算并填充 normal
数组。
模型导入 大多数 3D 建模软件(Blender, Maya 等)导出的模型文件(如 glTF, OBJ)会包含预计算好的法线数据,加载器会读入并设置好 geometry.normal
。
案例 - 可视化法线
通过ArrowHelper
箭头辅助器来可视化法线

添加一个立方体,通过 geometry.getAttribute('normal')
和geometry.getAttribute('position')
获取法线数据和顶点数据,拿到第一个顶点的法线向量和顶点向量
js
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshNormalMaterial()
)
scene.add(mesh);
const normal = geometry.getAttribute('normal');
const position = geometry.getAttribute('position');
const dir = new THREE.Vector3(normal.array[0], normal.array[1], normal.array[2]),
origin = new THREE.Vector3(position.array[0], position.array[1], position.array[2]);
const helper = new THREE.ArrowHelper( dir, origin, 2, 'deepskyblue' );
helper.position.copy(origin);
scene.add(helper);
案例 - 动态改变法线
先看效果

通过不断改变法线向量,观察立方体表面的变化
js
// 设置法向量
const setNormal = function (geometry, normalIndex, pos) {
const normal = geometry.getAttribute('normal');
normal.array[normalIndex * 3] = pos.x;
normal.array[normalIndex * 3 + 1] = pos.y;
normal.array[normalIndex * 3 + 2] = pos.z;
normal.needsUpdate = true;
}
const setArrowHelperToNormal = function (geometry, arrowHelper, normalIndex) {
const normal = geometry.getAttribute('normal');
const position = geometry.getAttribute('position');
const values1 = normal.array.slice(normalIndex * 3, normalIndex * 3 + 3);
const dir = new THREE.Vector3(values1[0], values1[1], values1[2]);
const values2 = position.array.slice(normalIndex * 3, normalIndex * 3 + 3);
const origin = new THREE.Vector3(values2[0], values2[1], values2[2]);
arrowHelper.setDirection(dir);
arrowHelper.position.copy(origin);
arrowHelper.setColor(new THREE.Color('deepskyblue'));
};
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const mesh = new THREE.Mesh( geometry, new THREE.MeshNormalMaterial({side: THREE.FrontSide}) );
scene.add( mesh );
const helper1 = new THREE.ArrowHelper();
const helper2 = new THREE.ArrowHelper();
const helper3 = new THREE.ArrowHelper();
scene.add(helper1);
scene.add(helper2);
scene.add(helper3);
const pos = {
x: -1,
y: -1,
z: 0
};
let radian = 0,
dps = 22.5,
lt = new Date();
const update = function () {
setNormal(geometry, 0, pos);
setNormal(geometry, 1, pos);
setNormal(geometry, 2, pos);
setArrowHelperToNormal(geometry, helper1, 0);
setArrowHelperToNormal(geometry, helper2, 1);
setArrowHelperToNormal(geometry, helper3, 2);
};
这里我们创建三个箭头辅助器,用来显示三个顶点的法线向量,setNormal
用来设置法向量,setArrowHelperToNormal
用来设置辅助器,然后我们可以在animation
函数中通过修改pos
对象,来实现箭头辅助器的移动
js
function animation() {
const now = new Date(),
secs = (now - lt) / 1000;
requestAnimationFrame( animation );
if (secs > 1 / 30) {
radian += Math.PI * 2 / 180 * dps * secs;
pos.y = Math.sin(radian);
pos.x = Math.cos(radian);
update();
renderer.render(scene, camera);
lt = now;
}
}