超详细——手把手教你用threejs实现一个酷炫的模型发光扫描效果(三)
上一篇文章已完成基本效果的实现,本文则完成整个项目的灵魂:发光效果以及模型优化。unrealBloom实现辉光效果接下来才是重头戏,前文基本功能已完成,但总有一种买家秀与卖家秀的感觉,接下来我们将通过辉光(UnrealBloom)实现效果美化,将买家秀彻底转变为买家秀。买家秀卖家秀先介绍一下辉光的实现原理:我们通常的三维场景是使用render渲染器渲染出来的,当我们想给三维场景加一层“滤镜”时,就
上一篇文章
voidjay,公众号:web前端可视化超详细——手把手教你用threejs实现一个酷炫的模型发光扫描效果(二)
上一篇文章已完成基本效果的实现,本文则完成整个项目的灵魂:发光效果以及模型优化。
unrealBloom实现辉光效果
接下来才是重头戏,前文基本功能已完成,但总有一种买家秀与卖家秀的感觉,接下来我们将通过辉光(UnrealBloom)实现效果美化,将买家秀彻底转变为买家秀。
买家秀
卖家秀
先介绍一下辉光的实现原理:
我们通常的三维场景是使用render渲染器渲染出来的,当我们想给三维场景加一层“滤镜”时,就需要使用后处理(post-processing)技术了,后处理可以给模型增加一个或多个图形效果,例如景深、发光、胶片微粒或是各种类型的抗锯齿等,UnrealBloom就是后处理的一种,UnrealBloom可以说是实现科技风必备的一种效果。
要使用后处理,需要先基于render添加一个效果合成器,在效果合成器中添加基本的渲染通道,也就是原场景和相机的通道,再添加辉光通道(UnrealBloomPass),最后将composer的render方法代替render的render方法就行了。整个流程跟我们PS图片类似,将原图作为底图,基于原图增加一些滤镜效果,最后将P过的图片替代原图进行保存。
由于我们生成的辉光的部分只有模型边缘轮廓及扫描线部分,因此我们需要实现局部辉光效果。局部辉光效果要比全局辉光复杂很多,我一点一点解释,局部辉光核心代码如下:
const bloomParams = {
exposure: 1,
bloomThreshold: 0,
bloomStrength: 1,
bloomRadius: 0.2,
};
const bloomVertext = `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`;
const bloomFragment = `
uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );
}
`;
function renderBloom() {
// 添加效果合成器
bloomComposer = new EffectComposer(renderer);
bloomComposer.renderToScreen = false;
// 添加基本的渲染通道
const renderPass = new RenderPass(scene, camera);
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight)
)
bloomPass.threshold = bloomParams.bloomThreshold;
bloomPass.strength = bloomParams.bloomStrength;
bloomPass.radius = bloomParams.bloomRadius;
bloomComposer.addPass(renderPass);
// 把通道加入到组合器
bloomComposer.addPass(bloomPass);
const finalPass = new ShaderPass(
new THREE.ShaderMaterial({
uniforms: {
baseTexture: { value: null },
bloomTexture: { value: bloomComposer.renderTarget2.texture },
},
vertexShader: bloomVertext,
fragmentShader: bloomFragment,
defines: {},
}),
'baseTexture'
);
finalPass.needsSwap = true;
// 初始化实际效果合成器
finalComposer = new EffectComposer(renderer);
finalComposer.addPass(renderPass);
finalComposer.addPass(finalPass);
}
我们需要添加两个效果合成器:bloomComposer及finalComposer,bloomComposer用于生成辉光材质,finalComposer用于渲染整个场景。
为了区分辉光对象和非辉光对象,我们需要改变其图层编号。将需要变为辉光的对象的图层编号设为1,其余默认为0,在原renderEffect方法中加入如下代码,注意带+号的部分:
// 区分辉光与非辉光层
const ENTIRE_SCENE = 0,
BLOOM_SCENE = 1;
const bloomLayer = new THREE.Layers();
bloomLayer.set(BLOOM_SCENE);
function renderEffect(model) {
let edgeGroup = new THREE.Group();
model.traverse((obj) => {
if(obj.name == '立方体') {
// 将扫描框转为辉光材质
+ obj.layers.toggle(BLOOM_SCENE);
let shaderMaterial = new THREE.ShaderMaterial({
transparent: true,
side: THREE.DoubleSide,
uniforms: {
height: scanConfig,
uFlowColor: {
value: new THREE.Vector4(0.0, 1.0, 1.0, 1.0),
},
uModelColor: {
value: new THREE.Vector4(0.0, 0.0, 0.0, 0.0),
},
},
vertexShader: uperVertext,
fragmentShader: uperFragment,
})
obj.material = shaderMaterial;
let boundingBox = obj.geometry.boundingBox;
// 初始化扫描配置,y轴上下需留出一定空间,防止把上下平面扫描出来
scanConfig.start = boundingBox.min.y+0.1 || 0;
scanConfig.end = boundingBox.max.y-0.1 || 0;
scanConfig.value = scanConfig.start;
}
// 设置样式
else
if(obj.type === 'Mesh') edgeGroup.add(_renderFrameMesh(obj));
});
scene.add(edgeGroup);
// 重置变换
function _renderFrameMesh(obj) {
const edges = new THREE.EdgesGeometry(obj.geometry);
let color = new THREE.Color(0.1, 0.3, 1);
var lineBasematerial = new THREE.LineBasicMaterial({
color: color,
side: THREE.FrontSide,
linecap: 'round', //ignored by WebGLRenderer
linejoin: 'round', //ignored by WebGLRenderer
});
const line = new THREE.LineSegments(edges, lineBasematerial);
// 将外框转为辉光材质
+ line.layers.toggle(BLOOM_SCENE);
return line;
}
}
最后,我们把不需要辉光的部分转为黑色材质,使其辉光效果失效。
// 将材质转为黑色材质
function darkenNonBloomed(obj) {
if (obj.isMesh && bloomLayer.test(obj.layers) === false) {
materials[obj.uuid] = obj.material;
obj.material = darkMaterial;
}
}
设置一个还原材质的方法,将转为黑色材质的物体进行还原。
// 还原材质
function restoreMaterial(obj) {
if (materials[obj.uuid]) {
obj.material = materials[obj.uuid];
delete materials[obj.uuid];
}
}
在render方法中先转换材质,生成辉光效果,然后还原材质,最后渲染整个场景,从而实现部分辉光的效果。
function bloomRender() {
scene.traverse((obj) => darkenNonBloomed(obj));
bloomComposer.render();
scene.traverse((obj) => restoreMaterial(obj));
finalComposer.render();
requestAnimationFrame(() => {
calcHeight()
bloomRender();
});
}
init()
// render()
bloomRender()
模型压缩
在web项目中,网页性能十分重要,对模型大小的控制也有着极为严苛的要求,普遍模型大小需小于10M,因此3d模型的压缩就是必须要考虑的了。本项目模型原大小为47.3M,经过压缩后为2.8M,如下图,大小降低了近20倍。
我们使用Draco对模型进行压缩,通过 glTF 配合 Draco 压缩的方式,可以在视觉效果近乎一致的情况下,让3D模型文件成倍缩小。下面具体介绍 glTF 格式及 Draco 压缩工具。
- glTF格式介绍
glTF 称为“ 3D 界的 JPEG”,使用了更优的数据结构,为应用程序实时渲染而生。glTF 有以下几大特点:
- 由现有 OpenGL 的维护组织 Khronos 推出,目的就是为了统一用于应用程序渲染的 3D 格式,更适用于基于 OpenGL 的引擎;
2. 减少了 3D 格式中除了与渲染无关的冗余信息,最小化 3D 文件资源;
3. 优化了应用程序读取效率和和减少渲染模型的运行时间;
4.支持 3D 模型几何体、材质、动画及场景、摄影机等信息。
glTF 导出格式有两种后缀格式可供选择:.gltf 和 .glb
glTF 文件导出时一般会输出两种文件类型,一是 .bin 文件,以二进制流的方式存储顶点坐标、顶点法线坐标和贴图纹理坐标、贴图信息等模型基本数据信息;二是 .gltf 文件,本质是 json 文件,记录对bin文件中模型顶点基本数据的索引、材质索引等信息,方便编辑,可读性较好;
glb 文件格式只导出一个 .glb 文件,将所有数据都输出为二进制流,通常来说会更小一点,若不关心模型内的具体数据可直接选择此类型。
另外,建议所有3d开发人员都要简单学习一下建模的基本知识!需要学习的建模软件在这里只推荐Blender,原因有几点:
-
Blender可以导出glTF模型,而很多建模软件是不能导出glTF模型的,项目开发时建模师可能会给你其他格式的模型,此时就需要自己转换一下了。
-
建模师给出的模型可能并不适合当前业务,如模型拆分,模型分组命名,模型材质等,都需要微调一下,这时候你自己顺手修改一下模型会节省很多时间。
-
不懂建模的3d开发人员就和不懂html的web前端开发人员一样,项目开发时,需要和建模师进行沟通磨合,如果你对建模一窍不通的话,项目就没办法推进下去了。
2. 模型压缩
Draco 是 Google 推出的一个用于 3D 模型压缩和解压缩的工具库,glTF 资源可通过基于 Draco 开发的命令行工具 gltf-pipeline 进行编码压缩,gltf-pipeline 可通过 npm 的方式安装使用。使用方法如下:
#全局安装
npm install -g gltf-pipeline
#压缩glb文件 -b表示输出glb格式,-d表示压缩
gltf-pipeline -i model.glb -b -d
#压缩glb文件并将纹理图片分离出来
gltf-pipeline -i model.glb -b -d -t
#更多参数查阅
gltf-pipeline -h
通过 Draco 进行压缩基本上是有损的,有两点表现:
- Draco 通过 Edge breaker 3D 压缩算法改变了模型的网格数据的索引方法,缺少了原来的网格顺序;
- Draco 通过减少顶点坐标、顶点纹理坐标等信息的位数,以减少数据的存储量。
本文我们使用命令:gltf-pipeline -i model.glb -b -d,对模型进行压缩,模型大小基本可以满足要求,如果贴图较大,可以分离出纹理图片,对纹理图片进行压缩处理,以更进一步减少模型大小。
- 纹理图片压缩
纹理图片压缩采用.basis格式,将 png 转换为 basis 文件后,大小与 jpg 格式差不多,但在 GPU 上比 png/jpg 小6-8倍。
可通过 basisu 命令行工具压缩 png,直接从github 官网下载Release版本或者通过 CMake 编译源码,以 Mac 系统为例(Windows 系统将命令改为 basis.exe),列举几种常用用法:
# 进入执行目录
cd bin-osx
# 将.png格式转为 .basis
./basisu xxx.png
# 针对法线/金属/粗糙贴图等linear颜色空间的贴图 需加上-linear
./basisu xxx.png -linear
# 最大限度保证图片质量的转换
./basisu xxx.png -comp_level 5 -max_endpoints 16128 -max_selectors 16128 -no_selector_rdo
# 最大限度压缩linear颜色空间的贴图
./basisu xxx.png -linear -global_sel_pal -no_hybrid_sel_cb
生成的 .basis 文件需要在程序中通过转码器转成设备的压缩纹理格式,例如在ThreeJS 中可通过 basisTextureLoader 转换,具体用法可查阅ThreeJS 官网。
至此,本项目已基本完成开发,大家有什么意见及问题欢迎在评论区交流!
欢迎关注我的公众号获取webgl资料及最新文章,您也可以添加我的微信进行沟通交流:
公众号:web前端可视化
微信:voidjay
更多推荐
所有评论(0)