使用threejs生成3d模型
认识3d
相关文档:three.js中文文档
一些基本名词
相机 Camera
提供视野
光源 Light
之所以能看见是因为光的反射,没有光源看到的模型是一片黑
常见光源:
- AmbientLight(环境光):影响整个场景,不影响阴影的生成,使用其他光源时使用环境光用于弱化阴影或添加颜色。用来烘托范围
- PointLight(点光源)
- DirectionalLight(方向光):直线光
- SpotLight(聚光灯光源):最常用的光源,锥形效果。
其次还有:HemisphereLight(半球光),AreaLight(面光源),LensFlare(镜头眩光)
材质 Material
常用材质:
- BasicMaterial 基础材质:设置材质颜色,不会受光照影响,不会有高光阴影部分。
- MeshLambertMaterial 漫反射材质:一种非光泽表面的材质,没有镜面高光。
- MeshPhongMaterial 镜面反射效果:有镜面反射的效果。其他属性与漫反射材质一样。
- StandardMaterial:一种基于物理的标准材质。既有漫反射射,也有高光。
常用的材质参数:
- color: 材质的颜色
- emissive:放射光的颜色
- emissiveIntensity:放射光强度。调节发光颜色。
- metalness:材质与金属的相似度。非金属材质,如木材或石材。
- refractionRatio:空气的折射率(IOR)(约为1)除以材质的折射率。
- roughness:材质的粗糙程度。0.0表示平滑的镜面反射,1.0表示完全漫反射。
其他: 纹理贴图 Texture,动画 Animation
3d模型文件
obj
OBJ文件适合用于3D软件模型之间的互导。目前几乎所有知名的3D软件都支持OBJ文件的读写。OBJ文件是一种文本文件,可以直接用写字板打开进行查看和编辑修改。
stl
STL文件是在计算机图形应用系统中,用于表示三角形网格的一种文件格式。 它的文件格式非常简单, 应用很广泛。STL是最多快速原型系统所应用的标准文件类型。STL是用三角网格来表现3D CAD模型。在STL文件中的三角面片的信息单元 facet 是一个带矢量方向的三角面片,STL三维模型就是由一系列这样的三角面片构成。
abc
用于解决特效界共同的问题, 可以共享复杂的动态场景, 跨越不同的软件之间, 这个格式命名为Alembic ,英文直译为蒸馏机。本质上就是一个CG交换格式, 专注于特效地储存, 共享动画与特效场景, 跨越不同的应用程式或是软件, 包含了商业贩售的软件或是公司内部开发的软件。
x3d
X3D是一种专为万维网而设计的三维图像标记语言。全称可扩展三维(语言),是由Web3D联盟设计的,是VRML标准的最新的升级版本。X3D基于XML格式开发,所以可以直接使用XML DOM文档树、XML Schema校验等技术和相关的XML 编辑工具。目前X3D已经是通过ISO认证的国际标准。
psk
是大名鼎鼎的虚幻游戏引擎的模型格式。虚幻游戏模型提取出来的就是psk格式。
glb/gltf
gltf是一种可以减少3D格式中与渲染无关的冗余数据并且在更加适合OpenGL簇加载的一种3D文件格式。
gltf的提出是源自于3D工业和媒体发展的过程中,对3D格式统一化的急迫需求。如果用一句话来描述:gltf 就是三维文件的JPEG ,三维格式的MP3。
gltf是对近二十年来各种3D格式的总结,使用最优的数据结构,来保证最大的兼容性以及可伸缩性。这就好比是本世纪初xml的提出。gltf使用json格式进行描述,也可以编译成二进制的内容:bgltf。gltf可以包括场景、摄像机、动画等,也可以包括网格、材质、纹理,甚至包括了渲染技术、着色器以及着色器程序。同时由于json格式的特点,它支持预留一般以及特定供应商的扩展。
关于glb文件的压缩
两个压缩工具:
gltf-pipeline:
采用网格压缩(KHR_draco_mesh_compression),最常见的一种网格压缩方式,采用开源的Draco算法,用于压缩和解压缩3D 网格和点云,并且可能会改变网格中顶点的顺序和数量。压缩使文件小得多,但是在客户端设备上需要额外的解码时间。
# 初始化
npm install gltf-pipeline -g
# 压缩glb文件
gltf-pipeline -i male.glb -o demo.glb -d
在代码中需要设置远程解码文件,解码文件加载也需要一定的时间。
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
...
constructor() {
this._loader = new ThreeGLTFLoader();
this._dracoLoader = new DRACOLoader();
const loader = this._loader;
loader.setCrossOrigin('anonymous');
const dracoLoader = this._dracoLoader;
// 设置远程解码文件
dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
loader.setDRACOLoader(dracoLoader);
}
...
创建解码器实例需要引入额外的库来进行解码,setDecoderPath
会自动请求 wasm 文件来进行解密操作。而这两个 wasm 文件同时也增加了请求时间和请求数量,那么加上这两个文件,真实的压缩率约为62.5% 。
该方法压缩率较高,但是需要远程加载解密文件,且加载时间较长。
gltfpack:
方法一:
KHR_mesh_quantization,顶点属性通常使用FLOAT
类型存储,将原始始浮点值转换为16位或8位存储以适应统一的3D或2D网格,也就是我们所说的quantization向量化,该插件主要就是将其向量化。
# 初始化
npm install gltfpack -g
# 压缩文件
gltfpack -i male.glb -o demo.glb
压缩率为原文件的59.3%,比原模型加载速度也快上不少。
方法二:
EXT_meshopt_compression,此插件假定缓冲区视图数据针对 GPU 效率进行了优化——使用量化并使用最佳数据顺序进行 GPU 渲染——并在 bufferView 数据之上提供一个压缩层。每个 bufferView 都是独立压缩的,这允许加载器最大程度地将数据直接解压缩到 GPU 存储中。
除了优化压缩率之外,压缩格式还具有两个特性——非常快速的解码(使用 WebAssembly SIMD,解码器在现代桌面硬件上以约 1 GB/秒的速度运行),以及与通用压缩兼容的字节存储。也就是说,不是尽可能地减少编码大小,而是以通用压缩器可以进一步压缩它的方式构建比特流。
gltfpack -i male.glb -o demo.glb -cc
压缩率为原文件的65.6% ,首次加载时间比原模型快上不少。
放到实际项目中,没有画质损失和加载时间过长的问题。
gltfpack压缩后需要使用解压器:
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module';
...
constructor() {
this._loader = new ThreeGLTFLoader();
this._dracoLoader = new DRACOLoader();
const loader = this._loader;
loader.setCrossOrigin('anonymous');
loader.setMeshoptDecoder(MeshoptDecoder);
loader.setDRACOLoader(dracoLoader);
}
...
经过测试,使用gltfpack的方法二,压缩效果最佳,且加载速度较快。
threejs加载glb文件
初始化
npm install three
Demo: -> webgl_animation_keyframes
demo源码解析
import * as THREE from '../build/three.module.js';
import Stats from './jsm/libs/stats.module.js';
import { OrbitControls } from './jsm/controls/OrbitControls.js';
import { RoomEnvironment } from './jsm/environments/RoomEnvironment.js';
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from './jsm/loaders/DRACOLoader.js';
Stats
:用于显示状态信息,比如实时显示fps,已帮只在开发环境用到;OrbitControls
:为控制器,用于控制3d模型旋转、缩放和位移;RoomEnvironment
:为环境,嗯也可以说是世界;GLTFLoader
:gltf文件加载器DRACOLoader
:数据解码器
接下来就是初始化:
// 时钟
const clock = new THREE.Clock();
// 容器 用于存放canvas视图
const container = document.getElementById( 'container' );
// 状态显示器
const stats = new Stats();
container.appendChild( stats.dom );
// 渲染器
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild( renderer.domElement );
const pmremGenerator = new THREE.PMREMGenerator( renderer );
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color( 0xbfe3dd );
scene.environment = pmremGenerator.fromScene( new RoomEnvironment(), 0.04 ).texture;
// 创建相机
const camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 100 );
// 设置相机位置
camera.position.set( 5, 2, 8 );
// 控制器 该操作器用于 手滑动交互
const controls = new OrbitControls( camera, renderer.domElement );
controls.target.set( 0, 0.5, 0 );
controls.update();
controls.enablePan = false;
controls.enableDamping = true;
上面这些是对场景的搭建,其中clock
是时钟,用于3d模型动画的正常运转。控制器可以实现3d模型的自转,以及用户的交互场景。
接下来就是加载3d模型文件:
let mixer;
// 解密器
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'js/libs/draco/gltf/' );
// gltf加载器
const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );
// 加载glb文件
loader.load( 'models/gltf/demo.glb', function ( gltf ) {
const model = gltf.scene;
// 重新设置模型位置
model.position.set( 1, 1, 0 );
// 缩放模型
model.scale.set( 0.01, 0.01, 0.01 );
// 将模型添加到场景中
scene.add( model );
// 获取模型动画
mixer = new THREE.AnimationMixer( model );
// 播放动画
mixer.clipAction( gltf.animations[ 0 ] ).play();
// 循环播放动画
animate();
}, undefined, function ( e ) {
console.error( e );
} );
模型加载步骤:初始化文件加载器 -> 初始化解码器 -> 加载文件 -> 文件加载成功,获取3d模型 -> 初始化模型位置、大小 -> 获取模型动画 -> 播放动画 -> 更新动画
然后就是animate
函数的实现:
function animate() {
// 循环熏染动画
requestAnimationFrame( animate );
// 获取时钟 滴答数
const delta = clock.getDelta();
// 更新动画
mixer.update( delta );
// 更新控制器
controls.update();
// 更新状态显示器
stats.update();
// 场景和相机 渲染到画布
renderer.render( scene, camera );
}
animate
函数里面包含的是所有与动画有关的时间步长的更新,包括模型自身的动画,控制器动画和状态信息的刷新,动画更新后需要重新进行渲染。
然后就是,但浏览器出口大小发生变化时,需要重新设置一下渲染器的宽高。
// 自适应屏幕大小
window.onresize = function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
};
进阶:编写渲染3d模型react组件
为避免重复造轮子,这里采用第三方库:view3d / 官网地址 / 文档
初始化
npm install @egjs/view3d
demo源码解析
基本结构:
import { useEffect, useRef } from 'react';
import { THREE } from '@egjs/view3d/consts/external';
import View3D, { AutoControl, OrbitControl, GLTFLoader } from '@egjs/view3d';
// 3d模型视图组件
// url: glb文件链接 lights 灯光 distance 相机高度 traverseCallBack 材质属性调整
export default function Three3DViewDemo(props: any) {
const { url, lights = [], distance, traverseCallBack, ...restProps } = props;
const ref = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!url || !ref.current) {
return;
}
// 生成3d视图
const view3d = new View3D(ref.current);
init(view3d);
}, [ref.current]);
return (
<div {...restProps}>
<canvas ref={ref} />
</div>
);
}
3d模型组件的基本结构和其他组件类似,这里主要是需要获取canvas,然后在canvas上面进行渲染。
初始化:
const init = async (view3d: View3D) => {
// 设置自动旋转的控制器
view3d.controller.add(
new AutoControl({
speed: 0.5, // 旋转速度
}),
);
// 设置交互控制器
view3d.controller.add(
new OrbitControl({
translate: {
scaleToElement: false,
useGrabCursor: false,
},
}),
);
// 加载glb文件
const loader = new GLTFLoader();
const model = await loader.load(url);
// 设置模型初始y轴角度:顺时针45读
model.scene.rotateY(Math.PI / 4);
// 材质遍历
model.scene.traverse((obj0: any) => {
if (traverseCallBack) {
traverseCallBack(obj0);
}
});
// 将模型添加到视图
view3d.display(model);
// 播放动画
view3d.animator.play(0);
// 设置相机高度 将影响模型呈现的大小
view3d.camera.distance = distance || 70;
// 添加灯光效果
const _linghts = lights || [];
if (_linghts.length) {
_linghts.forEach((l: THREE.Light) => view3d.scene.add(l));
}
// 默认加个直线光
const light = new THREE.DirectionalLight(0xffffff, 0.7);
light.position.set(1, 1, 1); // 设置光位置
view3d.scene.add(light); // 将光源添加到场景
view3d.on('beforeRender', e => {
// 光源跟随相机移动
light.position.copy(e.camera.threeCamera.position);
});
};
使用view3d之后初始化变得更加简单,一些代码逻辑已经被封装在库里面,需要关心的是差异的部分,比如添加灯光,设置模型材质参数,调节材质光滑度,以及添加一些想要的控制器。
进阶:关于3d模型的性能优化
一般3d模型从组件加载到销毁的过程比较简洁。但是在3d视图比较多的的情况下,尤其是在ios手机上,存在严重的问题,内存会一直暴增,直到页面崩溃。
这里给出一些建议:
- 增加异常兜底,模型异常后需要删除全局的3d视图,避免下次渲染再次出现异常
- 采用全局的canvas,canvas实现重复利用,避免视图切换的时候反复创建canvas,造成资源的浪费
- 模型增加缓存队列,队列缓存个数虽然有限,但是在用户快速切换的时候,可以快速展示加载好的模型
- 3d模型加载后,并不直接渲染,而是显示预览图片,需要用户手动点击才渲染,这个和视频的原理是一样的,延迟渲染,可以有充足的时间加载3d模型,同时可以避免用户频繁切换3d组件,造成渲染吃力,内存增加过快的问题(这个问题存在与安卓端)
- 同理,设置全局3d视图,也是为了避免频繁的初始化,造成不必要的内存增加
转载自:https://juejin.cn/post/7161742684229468190