likes
comments
collection

使用threejs生成3d模型

作者站长头像
站长
· 阅读数 34

认识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文件的压缩

参考:3D性能优化 | 说一说glTF文件压缩

两个压缩工具:

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视图,也是为了避免频繁的初始化,造成不必要的内存增加