threejs后处理实现轮廓线功能
鼠标移到光伏基站里的配电箱上,箱体亮起一圈黄边;移到旁边的汇流箱上,边就跟着换到汇流箱。这一圈边就是轮廓线,three.js 里对应的东西是后处理里的 OutlinePass。
先把一个容易混的地方分开:这圈边是”画”出来的,”鼠标在哪个箱体上”是”算”出来的。 判断鼠标指着谁用的是 Raycaster 射线拾取,把那个物体描一圈用的是 OutlinePass。两件事经常一起出现,但互不认识——OutlinePass 全程不关心鼠标,它只知道”这批物体要描边”。
这篇只讲描边那一半。
1. EffectComposer:渲染不再直接上屏
后处理的意思是:场景先不画到屏幕,而是画到一张离屏纹理上,然后一串”通道”依次在这张纹理上做操作,最后一个通道才画到屏幕。
1 | 不用后处理 |
要改的就一行:renderer.render(scene, camera) 换成 composer.render()。这条最容易漏——少改这一行,整条链搭得再对也白搭,画面还是原样。
“最后画到屏幕”这件事不用手动指定,EffectComposer 会把 renderToScreen 分给最后一个启用的通道。所以 OutputPass 必须放在最后。
2. OutlinePass 描的是什么
它不看几何。整个流程靠两张东西:
1 | 选中物体的遮罩 整个场景的深度 / 法线 |
两个结论直接从这张图来:
- 它描的是”选中物体在屏幕上占的那块区域”的边界,所以跟这个物体有多少条棱边完全无关。一个一万面的汇流箱和一个立方体,描出来都是光滑的一圈。
- 被挡住的那部分轮廓会单独上色,也就是
hiddenEdgeColor。这就是为什么描边可以做成”透视”效果:汇流箱被前面的配电箱挡住时,轮廓从遮挡物里面透出来。不想要这个效果,把hiddenEdgeColor设成接近背景的颜色就行——它是”描成另一个颜色”,不是”关掉”。
3. 那七个参数
| 参数 | 默认值 | 管什么 |
|---|---|---|
edgeStrength |
3 |
描边的强度。越大越亮越”实”,调到 0 等于不描 |
edgeThickness |
1 |
描边的粗细 |
edgeGlow |
0 |
外发光。大于 0 会在硬边之外再加一层晕 |
pulsePeriod |
0 |
呼吸周期。0 是不呼吸,大于 0 就按这个周期明暗脉动 |
visibleEdgeColor |
0xffffff |
没被挡住那部分轮廓的颜色 |
hiddenEdgeColor |
0x190a05 |
被挡住那部分轮廓的颜色 |
downSampleRatio |
2 |
描边在 1/2 分辨率下算,省性能 |
usePatternTexture |
false |
用一张纹理当描边图案,不用纯色 |
edgeThickness 不是像素值,它是采样偏移的倍数;描边本身又在 downSampleRatio 降采样的图上算。所以别指望”设成 4 就是 4 像素粗”,它只保证”调大明显更粗”。要精确控制宽度,得改 downSampleRatio 和着色器里的偏移基数,一般没必要——悬停高亮对上视觉就行。
pulsePeriod 单独说一下:悬停高亮通常希望它是稳的,鼠标停在哪就一直是那一圈,所以设 0。要做”吸引注意力”的呼吸效果才给它一个正数,比如 3 就是 3 秒一次明暗循环。
4. 完整代码
1 | import * as THREE from 'three' |
selectedObjects 就是一个普通数组,改它立刻生效,不需要通知谁。
OutputPass 的作用是把 renderer.toneMapping 和输出色彩空间在链尾补上。它从 r152 开始有,之前的做法是自己在链尾加一个 ShaderPass 挂 GammaCorrectionShader。漏了它的典型症状是整幅画面偏色——场景看着比 renderer.render 那会儿灰或者暗。
5. 接上鼠标:悬停高亮
描边和拾取合起来才是完整的悬停效果:
1 | const raycaster = new THREE.Raycaster() |
四个点:
pointer要转成 NDC(-1 到 1),用getBoundingClientRect()取画布位置而不是innerWidth,否则画布不占满窗口时会偏。pickables传显式数组,recursive传false。 对整棵场景树做射线检测既慢,又会命中你看不见的东西(背景板、地面网格)。把能高亮的设备(配电箱、汇流箱这些)单独收进一个数组。- 放进
selectedObjects的应该是那块 mesh 本身,也就是hits[0].object。如果模型做了分组,别往里放父级Group。 - 悬停和点击选中要分开记。 如果两者都要,别共用一份数组——维护一个
hovered,一个picked,每次变化时把两者拼起来赋给selectedObjects。否则鼠标一移开,之前点选的东西也跟着灭了。
6. 一句话
描边和拾取是两件事:Raycaster 决定”描谁”,OutlinePass 决定”怎么描”。
真正要记住的只有三件事:描边是后处理,一次全屏 pass,跟物体有多少面无关;它描的是屏幕上的外轮廓,跟物体有多少条棱边无关;悬停高亮里那圈边和判定鼠标位置的那次射线,中间唯一的联系是”谁被放进了 selectedObjects“。
参考
- OutlinePass 文档:https://threejs.org/docs/#examples/en/postprocessing/OutlinePass
- 官方示例
webgl_postprocessing_outline - OutlinePass 源码:https://github.com/mrdoob/three.js/blob/dev/examples/jsm/postprocessing/OutlinePass.js
- EffectComposer 文档:https://threejs.org/docs/#examples/en/postprocessing/EffectComposer
threejs后处理实现轮廓线功能