Three.js-硬要自学系列40之专项学习缓冲几何体法线属性

理解法线属性

想象一下, 我们有一个 3D 模型,比如一个光滑的球体或者一个立方体。这个模型是由很多很多小三角形(面)拼成的。

每个小三角形有三个尖角,我们管它叫"顶点"(Vertex) 所以模型上有成百上千个顶点。 现在我们想给这个模型打光(比如放个灯泡在场景里)

那么问题来了,灯光照在模型的某个点上,到底应该有多亮? 这取决于这个点面向灯光的方向

法线(Normal)就是解决这个问题的"方向指示器"

法线是什么

法线是一个向量(有方向的箭头)。想象在每个顶点上(或者在每个三角形的中心)插着一根非常非常小的针。

它指向哪里

这根"针"(法线)垂直地指向模型表面的"外面" 。就像你用手指垂直地戳一个物体的表面,

它有什么用

当灯光照射过来时,渲染器(WebGL)会做以下计算:

  1. 看看灯光射过来的方向
  2. 看看这个顶点表面的法线方向
  3. 计算灯光方向法线方向 之间的夹角
  • 如果法线正对着灯光(夹角接近 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;
    }
}
相关推荐
用户2141183263602几秒前
dify案例分享-告别手工录入!Dify 工作流一键生成发票申请预览,对接开票系统超简单
前端
子龙_2 分钟前
JS解析wav音频数据并使用wasm加速
前端·javascript·音视频开发
爱吃大橘5 分钟前
到底用 `Promise.reject` 还是 `throw new Error`
前端·javascript·面试
前端进阶者9 分钟前
浏览器绿屏仅关闭关video硬件加速
前端
小蘑菇20189 分钟前
mac前端环境安装
前端
aningxiaoxixi16 分钟前
android audio 之 Engine
android·前端·javascript
枣仁_19 分钟前
关于 `lodash.camelCase` 与 `type-fest` 差异的深度分析
前端
码农小菲29 分钟前
告别虚拟 DOM?Vue3.6 Vapor 模式的性能革命与实践
前端·javascript·vue.js
阿奇__35 分钟前
uniapp 类似popover气泡下拉框组件
前端·css·uni-app
Hilaku35 分钟前
从 jQuery 到 React 再到 Svelte:我眼中的前端组件化演进史
前端·javascript·svelte